T-CREATOR

Redux Toolkit から Jotai への移行は可能か?具体的なリファクタリング戦略とコード例

Redux Toolkit から Jotai への移行は可能か?具体的なリファクタリング戦略とコード例

React アプリケーションの状態管理は、開発者にとって常に重要な課題です。Redux Toolkit は長年、大規模アプリケーションの状態管理の標準として君臨してきました。しかし、React の進化とともに、より軽量で直感的な状態管理ライブラリが登場しています。

Jotai は、その中でも特に注目を集めているライブラリの一つです。原子(Atom)ベースの状態管理により、Redux の複雑さを排除しながら、同じレベルの機能を提供します。

この記事では、Redux Toolkit から Jotai への移行が本当に可能なのか、そしてどのように実現するのかを具体的なコード例とともに解説します。移行を検討している開発者の方々にとって、実践的で価値のある情報をお届けします。

状態管理ライブラリの進化

React の状態管理は、コンポーネントのuseStateから始まり、Context API、Redux、そして現在の Jotai や Zustand へと進化してきました。

Redux の時代

Redux は、予測可能な状態管理を提供し、大規模アプリケーションでの状態の一貫性を保証しました。しかし、その複雑さは多くの開発者を悩ませてきました。

typescript// Reduxの典型的な実装例
const initialState = {
  todos: [],
  loading: false,
  error: null,
};

const todoSlice = createSlice({
  name: 'todos',
  initialState,
  reducers: {
    addTodo: (state, action) => {
      state.todos.push(action.payload);
    },
    setLoading: (state, action) => {
      state.loading = action.payload;
    },
  },
});

このコードを見ると、Redux の特徴的な構造がわかります。createSliceによる定型文、reducersの定義、そしてaction.payloadの使用。これらは強力ですが、学習コストが高いという課題があります。

現代の状態管理

Jotai は、この複雑さを排除し、より直感的な API を提供します。

typescript// Jotaiでの同等の実装
const todosAtom = atom<Todo[]>([]);
const loadingAtom = atom<boolean>(false);
const errorAtom = atom<string | null>(null);

// 派生状態の作成
const addTodoAtom = atom(
  null,
  (get, set, newTodo: Todo) => {
    const currentTodos = get(todosAtom);
    set(todosAtom, [...currentTodos, newTodo]);
  }
);

Jotai の特徴は、状態を「原子(Atom)」として扱うことです。これにより、状態の依存関係が明確になり、不要な再レンダリングを防ぐことができます。

Redux Toolkit から Jotai への移行の意義

なぜ移行を検討するのか

Redux Toolkit は確かに強力なツールですが、現代の React 開発においては過剰な場合があります。特に以下の点で課題を感じる開発者が増えています。

ボイラープレートコードの多さ

typescript// Redux Toolkitでの典型的なボイラープレート
const todoSlice = createSlice({
  name: 'todos',
  initialState: {
    items: [],
    loading: false,
    error: null,
  },
  reducers: {
    addTodo: (state, action) => {
      state.items.push(action.payload);
    },
    removeTodo: (state, action) => {
      state.items = state.items.filter(
        (todo) => todo.id !== action.payload
      );
    },
    setLoading: (state, action) => {
      state.loading = action.payload;
    },
    setError: (state, action) => {
      state.error = action.payload;
    },
  },
});

export const { addTodo, removeTodo, setLoading, setError } =
  todoSlice.actions;
export default todoSlice.reducer;

このコードは機能しますが、単純な状態更新のために多くのコードが必要です。

Jotai での同等実装

typescript// Jotaiでの簡潔な実装
const todosAtom = atom<Todo[]>([]);
const loadingAtom = atom<boolean>(false);
const errorAtom = atom<string | null>(null);

// アクションは関数として定義
const addTodoAtom = atom(null, (get, set, todo: Todo) => {
  const currentTodos = get(todosAtom);
  set(todosAtom, [...currentTodos, todo]);
});

const removeTodoAtom = atom(
  null,
  (get, set, todoId: string) => {
    const currentTodos = get(todosAtom);
    set(
      todosAtom,
      currentTodos.filter((todo) => todo.id !== todoId)
    );
  }
);

移行のメリット

1. バンドルサイズの削減

Redux Toolkit は約 13KB(gzip 圧縮後)ですが、Jotai は約 3KB です。これは約 77%の削減になります。

2. 学習コストの低下

Redux の概念(Action、Reducer、Store、Dispatch)を理解する必要がなく、React のuseStateに近い感覚で使用できます。

3. TypeScript との親和性

Jotai は最初から TypeScript を考慮して設計されており、型推論が優れています。

技術的比較と選択基準

アーキテクチャの違い

Redux と Jotai の根本的な違いは、状態管理のアプローチにあります。

Redux: 集中型状態管理

typescript// Reduxの状態構造
interface RootState {
  todos: {
    items: Todo[];
    loading: boolean;
    error: string | null;
  };
  user: {
    profile: UserProfile;
    isAuthenticated: boolean;
  };
  settings: {
    theme: 'light' | 'dark';
    language: string;
  };
}

Redux では、すべての状態が一つの大きなオブジェクトに格納されます。これにより、状態の予測可能性は高まりますが、状態の分割や再利用が困難になります。

Jotai: 分散型状態管理

typescript// Jotaiの状態構造
const todosAtom = atom<Todo[]>([]);
const userAtom = atom<UserProfile | null>(null);
const themeAtom = atom<'light' | 'dark'>('light');

// 派生状態の作成
const completedTodosAtom = atom((get) =>
  get(todosAtom).filter((todo) => todo.completed)
);

const todoCountAtom = atom((get) => get(todosAtom).length);

Jotai では、状態を小さな単位(Atom)に分割し、必要に応じて組み合わせることができます。

パフォーマンス比較

Redux の再レンダリング問題

typescript// Reduxでの問題のある実装例
const TodoList = () => {
  const { todos, loading, error } = useSelector(
    (state) => state.todos
  );

  // todosが変更されると、loadingやerrorも再計算される
  return (
    <div>
      {loading && <Spinner />}
      {error && <ErrorMessage error={error} />}
      {todos.map((todo) => (
        <TodoItem key={todo.id} todo={todo} />
      ))}
    </div>
  );
};

この実装では、todosが変更されると、loadingerrorが変更されていなくても、コンポーネント全体が再レンダリングされます。

Jotai での最適化

typescript// Jotaiでの最適化された実装
const TodoList = () => {
  const todos = useAtomValue(todosAtom);
  const loading = useAtomValue(loadingAtom);
  const error = useAtomValue(errorAtom);

  return (
    <div>
      {loading && <Spinner />}
      {error && <ErrorMessage error={error} />}
      {todos.map((todo) => (
        <TodoItem key={todo.id} todo={todo} />
      ))}
    </div>
  );
};

// 個別のコンポーネントで状態を購読
const TodoItem = ({ todo }: { todo: Todo }) => {
  const updateTodo = useSetAtom(updateTodoAtom);

  return (
    <div
      onClick={() =>
        updateTodo(todo.id, { completed: !todo.completed })
      }
    >
      {todo.title}
    </div>
  );
};

Jotai では、各 Atom が独立して管理されるため、必要な部分のみが再レンダリングされます。

移行戦略の全体像

事前準備

移行を開始する前に、現在の Redux アプリケーションを徹底的に分析する必要があります。

1. 状態構造の分析

typescript// 現在のRedux状態を分析するためのヘルパー関数
const analyzeReduxState = (state: RootState) => {
  const analysis = {
    totalStateKeys: Object.keys(state).length,
    nestedLevels: 0,
    circularReferences: false,
    largeObjects: [] as string[],
  };

  // 状態の深さを分析
  const analyzeDepth = (
    obj: any,
    path: string = '',
    depth: number = 0
  ) => {
    analysis.nestedLevels = Math.max(
      analysis.nestedLevels,
      depth
    );

    if (typeof obj === 'object' && obj !== null) {
      Object.keys(obj).forEach((key) => {
        const newPath = path ? `${path}.${key}` : key;
        analyzeDepth(obj[key], newPath, depth + 1);
      });
    }
  };

  analyzeDepth(state);
  return analysis;
};

2. 依存関係の特定

typescript// Reduxの依存関係を可視化する
const mapStateToProps = (state: RootState) => ({
  todos: state.todos.items,
  user: state.user.profile,
  theme: state.settings.theme,
});

// この依存関係をJotaiのAtomに変換する計画を立てる
const plannedAtoms = {
  todos: 'todosAtom',
  user: 'userAtom',
  theme: 'themeAtom',
};

移行計画

段階的な移行により、リスクを最小限に抑えることができます。

フェーズ 1: 新機能での Jotai 導入

既存の Redux コードはそのままに、新機能では Jotai を使用します。

typescript// 新機能でのJotai使用例
const newFeatureAtom = atom({
  data: null,
  loading: false,
  error: null,
});

const fetchNewFeatureAtom = atom(null, async (get, set) => {
  set(newFeatureAtom, (prev) => ({
    ...prev,
    loading: true,
  }));
  try {
    const data = await api.fetchNewFeature();
    set(newFeatureAtom, {
      data,
      loading: false,
      error: null,
    });
  } catch (error) {
    set(newFeatureAtom, {
      data: null,
      loading: false,
      error: error.message,
    });
  }
});

フェーズ 2: 段階的な置き換え

小さな機能から順次移行します。

typescript// 移行対象の優先順位付け
const migrationPriority = [
  { feature: 'theme', complexity: 'low', impact: 'medium' },
  {
    feature: 'user-preferences',
    complexity: 'low',
    impact: 'low',
  },
  {
    feature: 'todos',
    complexity: 'medium',
    impact: 'high',
  },
  {
    feature: 'authentication',
    complexity: 'high',
    impact: 'high',
  },
];

実行手順

1. Jotai のセットアップ

bash# Jotaiのインストール
yarn add jotai

# 開発用ツール(オプション)
yarn add jotai-devtools

2. Provider の設定

typescript// App.tsxでの設定
import { Provider } from 'jotai';

function App() {
  return (
    <Provider>
      <TodoApp />
    </Provider>
  );
}

3. 最初の Atom の作成

typescript// 最もシンプルな状態から始める
const themeAtom = atom<'light' | 'dark'>('light');

// テーマ切り替えのアクション
const toggleThemeAtom = atom(null, (get, set) => {
  const currentTheme = get(themeAtom);
  set(
    themeAtom,
    currentTheme === 'light' ? 'dark' : 'light'
  );
});

検証と最適化

1. パフォーマンステスト

typescript// 移行前後のパフォーマンスを比較
const performanceTest = () => {
  const startTime = performance.now();

  // 状態更新のテスト
  for (let i = 0; i < 1000; i++) {
    // 状態更新処理
  }

  const endTime = performance.now();
  return endTime - startTime;
};

2. メモリ使用量の監視

typescript// メモリ使用量の監視
const monitorMemoryUsage = () => {
  if ('memory' in performance) {
    const memory = (performance as any).memory;
    console.log('Memory usage:', {
      used: memory.usedJSHeapSize,
      total: memory.totalJSHeapSize,
      limit: memory.jsHeapSizeLimit,
    });
  }
};

実装パターンの変換

基本的な状態管理

Redux Toolkit での実装

typescript// Redux Toolkitでのカウンター実装
const counterSlice = createSlice({
  name: 'counter',
  initialState: 0,
  reducers: {
    increment: (state) => state + 1,
    decrement: (state) => state - 1,
    incrementByAmount: (state, action) =>
      state + action.payload,
  },
});

export const { increment, decrement, incrementByAmount } =
  counterSlice.actions;
export default counterSlice.reducer;

Jotai での同等実装

typescript// Jotaiでのカウンター実装
const counterAtom = atom(0);

const incrementAtom = atom(null, (get, set) =>
  set(counterAtom, get(counterAtom) + 1)
);

const decrementAtom = atom(null, (get, set) =>
  set(counterAtom, get(counterAtom) - 1)
);

const incrementByAmountAtom = atom(
  null,
  (get, set, amount: number) =>
    set(counterAtom, get(counterAtom) + amount)
);

コンポーネントでの使用

typescript// Reduxでの使用
const Counter = () => {
  const count = useSelector((state) => state.counter);
  const dispatch = useDispatch();

  return (
    <div>
      <span>Count: {count}</span>
      <button onClick={() => dispatch(increment())}>
        +
      </button>
      <button onClick={() => dispatch(decrement())}>
        -
      </button>
    </div>
  );
};

// Jotaiでの使用
const Counter = () => {
  const count = useAtomValue(counterAtom);
  const increment = useSetAtom(incrementAtom);
  const decrement = useSetAtom(decrementAtom);

  return (
    <div>
      <span>Count: {count}</span>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
};

複雑な状態構造

Redux Toolkit での複雑な状態

typescript// Reduxでの複雑な状態管理
interface TodoState {
  items: Todo[];
  filters: {
    status: 'all' | 'active' | 'completed';
    priority: 'low' | 'medium' | 'high' | 'all';
  };
  sorting: {
    field: 'title' | 'priority' | 'createdAt';
    direction: 'asc' | 'desc';
  };
  pagination: {
    page: number;
    limit: number;
    total: number;
  };
}

const todoSlice = createSlice({
  name: 'todos',
  initialState: {
    items: [],
    filters: { status: 'all', priority: 'all' },
    sorting: { field: 'createdAt', direction: 'desc' },
    pagination: { page: 1, limit: 10, total: 0 },
  } as TodoState,
  reducers: {
    addTodo: (state, action) => {
      state.items.push(action.payload);
    },
    updateFilters: (state, action) => {
      state.filters = {
        ...state.filters,
        ...action.payload,
      };
    },
    updateSorting: (state, action) => {
      state.sorting = {
        ...state.sorting,
        ...action.payload,
      };
    },
  },
});

Jotai での同等実装

typescript// Jotaiでの複雑な状態管理
const todosAtom = atom<Todo[]>([]);
const filtersAtom = atom({
  status: 'all' as const,
  priority: 'all' as const,
});
const sortingAtom = atom({
  field: 'createdAt' as const,
  direction: 'desc' as const,
});
const paginationAtom = atom({
  page: 1,
  limit: 10,
  total: 0,
});

// 派生状態の作成
const filteredTodosAtom = atom((get) => {
  const todos = get(todosAtom);
  const filters = get(filtersAtom);

  return todos.filter((todo) => {
    if (
      filters.status !== 'all' &&
      todo.status !== filters.status
    )
      return false;
    if (
      filters.priority !== 'all' &&
      todo.priority !== filters.priority
    )
      return false;
    return true;
  });
});

const sortedTodosAtom = atom((get) => {
  const filteredTodos = get(filteredTodosAtom);
  const sorting = get(sortingAtom);

  return [...filteredTodos].sort((a, b) => {
    const aValue = a[sorting.field];
    const bValue = b[sorting.field];

    if (sorting.direction === 'asc') {
      return aValue > bValue ? 1 : -1;
    } else {
      return aValue < bValue ? 1 : -1;
    }
  });
});

const paginatedTodosAtom = atom((get) => {
  const sortedTodos = get(sortedTodosAtom);
  const pagination = get(paginationAtom);

  const start = (pagination.page - 1) * pagination.limit;
  const end = start + pagination.limit;

  return sortedTodos.slice(start, end);
});

非同期処理

Redux Toolkit での非同期処理

typescript// Redux Toolkitでの非同期処理
export const fetchTodos = createAsyncThunk(
  'todos/fetchTodos',
  async (_, { rejectWithValue }) => {
    try {
      const response = await fetch('/api/todos');
      const data = await response.json();
      return data;
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);

const todoSlice = createSlice({
  name: 'todos',
  initialState: {
    items: [],
    loading: false,
    error: null,
  },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchTodos.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchTodos.fulfilled, (state, action) => {
        state.loading = false;
        state.items = action.payload;
      })
      .addCase(fetchTodos.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload as string;
      });
  },
});

Jotai での非同期処理

typescript// Jotaiでの非同期処理
const todosAtom = atom<Todo[]>([]);
const loadingAtom = atom<boolean>(false);
const errorAtom = atom<string | null>(null);

const fetchTodosAtom = atom(null, async (get, set) => {
  set(loadingAtom, true);
  set(errorAtom, null);

  try {
    const response = await fetch('/api/todos');
    const data = await response.json();
    set(todosAtom, data);
  } catch (error) {
    set(errorAtom, error.message);
  } finally {
    set(loadingAtom, false);
  }
});

// より高度な非同期処理パターン
const asyncTodosAtom = atom(async () => {
  const response = await fetch('/api/todos');
  return response.json();
});

// エラーハンドリング付きの非同期Atom
const safeAsyncTodosAtom = atom(async (get) => {
  try {
    const response = await fetch('/api/todos');
    if (!response.ok) {
      throw new Error(
        `HTTP error! status: ${response.status}`
      );
    }
    return await response.json();
  } catch (error) {
    console.error('Failed to fetch todos:', error);
    throw error;
  }
});

副作用の管理

Redux での副作用管理

typescript// Reduxでの副作用(ミドルウェア使用)
const todoMiddleware = (store) => (next) => (action) => {
  const result = next(action);

  if (action.type === 'todos/addTodo') {
    // ローカルストレージに保存
    const todos = store.getState().todos.items;
    localStorage.setItem('todos', JSON.stringify(todos));
  }

  return result;
};

// または、createAsyncThunkでの副作用
export const addTodoWithSideEffect = createAsyncThunk(
  'todos/addTodoWithSideEffect',
  async (todo: Todo, { dispatch, getState }) => {
    // APIに保存
    const response = await fetch('/api/todos', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(todo),
    });

    const savedTodo = await response.json();

    // ローカルストレージにも保存
    const currentTodos = getState().todos.items;
    localStorage.setItem(
      'todos',
      JSON.stringify([...currentTodos, savedTodo])
    );

    return savedTodo;
  }
);

Jotai での副作用管理

typescript// Jotaiでの副作用管理
const todosAtom = atom<Todo[]>([]);

// 副作用付きのAtom
const todosWithSideEffectAtom = atom(
  (get) => get(todosAtom),
  (get, set, newTodos: Todo[]) => {
    set(todosAtom, newTodos);

    // ローカルストレージに保存
    localStorage.setItem('todos', JSON.stringify(newTodos));
  }
);

// より高度な副作用パターン
const persistentTodosAtom = atom(
  (get) => get(todosAtom),
  (get, set, newTodos: Todo[]) => {
    set(todosAtom, newTodos);

    // 複数の副作用を実行
    Promise.all([
      localStorage.setItem(
        'todos',
        JSON.stringify(newTodos)
      ),
      fetch('/api/todos/sync', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(newTodos),
      }),
    ]).catch((error) => {
      console.error('Failed to persist todos:', error);
    });
  }
);

// 初期化時の副作用
const initializeTodosAtom = atom(null, async (get, set) => {
  try {
    // ローカルストレージから読み込み
    const stored = localStorage.getItem('todos');
    if (stored) {
      const todos = JSON.parse(stored);
      set(todosAtom, todos);
    }

    // サーバーから最新データを取得
    const response = await fetch('/api/todos');
    const serverTodos = await response.json();
    set(todosAtom, serverTodos);
  } catch (error) {
    console.error('Failed to initialize todos:', error);
  }
});

段階的移行の実践例

小さな機能から始める

移行は最もシンプルな機能から始めることをお勧めします。例えば、テーマ設定やユーザー設定など、他の機能への影響が少ない部分から始めましょう。

ステップ 1: テーマ機能の移行

typescript// 既存のReduxテーマ実装
const themeSlice = createSlice({
  name: 'theme',
  initialState: 'light',
  reducers: {
    toggleTheme: (state) =>
      state === 'light' ? 'dark' : 'light',
  },
});

// Jotaiでの新しい実装
const themeAtom = atom<'light' | 'dark'>('light');
const toggleThemeAtom = atom(null, (get, set) => {
  const currentTheme = get(themeAtom);
  const newTheme =
    currentTheme === 'light' ? 'dark' : 'light';
  set(themeAtom, newTheme);

  // 副作用:CSSクラスの更新
  document.documentElement.setAttribute(
    'data-theme',
    newTheme
  );
});

ステップ 2: コンポーネントの更新

typescript// 既存のReduxコンポーネント
const ThemeToggle = () => {
  const theme = useSelector((state) => state.theme);
  const dispatch = useDispatch();

  return (
    <button onClick={() => dispatch(toggleTheme())}>
      Current theme: {theme}
    </button>
  );
};

// Jotaiでの新しいコンポーネント
const ThemeToggle = () => {
  const theme = useAtomValue(themeAtom);
  const toggleTheme = useSetAtom(toggleThemeAtom);

  return (
    <button onClick={toggleTheme}>
      Current theme: {theme}
    </button>
  );
};

段階的な置き換え

ハイブリッドアプローチ

移行期間中は、Redux と Jotai を併用することができます。

typescript// ReduxとJotaiの橋渡し
const bridgeAtom = atom(
  (get) => {
    // Reduxの状態を監視(実際の実装では適切な方法を使用)
    return get(reduxStateAtom);
  },
  (get, set, newValue) => {
    // Reduxのアクションをディスパッチ
    set(reduxDispatchAtom, updateReduxAction(newValue));
  }
);

// 移行中のコンポーネント
const HybridComponent = () => {
  const [localState, setLocalState] = useAtom(localAtom);
  const reduxState = useSelector(
    (state) => state.someFeature
  );

  return (
    <div>
      <LocalFeature
        state={localState}
        setState={setLocalState}
      />
      <ReduxFeature state={reduxState} />
    </div>
  );
};

状態の同期

typescript// ReduxとJotaiの状態を同期する
const syncAtom = atom(
  null,
  (get, set, action: 'syncFromRedux' | 'syncToRedux') => {
    if (action === 'syncFromRedux') {
      // Reduxの状態をJotaiに同期
      const reduxState = get(reduxStateAtom);
      set(jotaiStateAtom, reduxState);
    } else {
      // Jotaiの状態をReduxに同期
      const jotaiState = get(jotaiStateAtom);
      set(reduxDispatchAtom, updateReduxAction(jotaiState));
    }
  }
);

テスト戦略

Jotai のテスト

typescript// Jotaiのテスト例
import { renderHook } from '@testing-library/react';
import { Provider } from 'jotai';
import { useAtom } from 'jotai';

const TestWrapper = ({ children }) => (
  <Provider>{children}</Provider>
);

describe('Jotai Atoms', () => {
  it('should update counter correctly', () => {
    const { result } = renderHook(
      () => useAtom(counterAtom),
      {
        wrapper: TestWrapper,
      }
    );

    const [count, setCount] = result.current;

    expect(count).toBe(0);
    act(() => setCount(5));
    expect(result.current[0]).toBe(5);
  });

  it('should handle async operations', async () => {
    const { result } = renderHook(
      () => useAtom(asyncTodosAtom),
      {
        wrapper: TestWrapper,
      }
    );

    // 初期状態はPromise
    expect(result.current[0]).toBeInstanceOf(Promise);

    // 非同期処理の完了を待つ
    await waitFor(() => {
      expect(result.current[0]).not.toBeInstanceOf(Promise);
    });
  });
});

移行前後のテスト比較

typescript// 移行前のReduxテスト
describe('Redux Todos', () => {
  it('should add todo', () => {
    const store = configureStore({
      reducer: { todos: todoReducer },
    });

    store.dispatch(
      addTodo({ id: '1', title: 'Test', completed: false })
    );

    const state = store.getState();
    expect(state.todos.items).toHaveLength(1);
    expect(state.todos.items[0].title).toBe('Test');
  });
});

// 移行後のJotaiテスト
describe('Jotai Todos', () => {
  it('should add todo', () => {
    const { result } = renderHook(
      () => useAtom(todosAtom),
      {
        wrapper: TestWrapper,
      }
    );

    const [todos, setTodos] = result.current;
    const addTodo = useSetAtom(addTodoAtom);

    act(() => {
      addTodo({ id: '1', title: 'Test', completed: false });
    });

    expect(result.current[0]).toHaveLength(1);
    expect(result.current[0][0].title).toBe('Test');
  });
});

移行後の運用と保守

パフォーマンス監視

移行後は、アプリケーションのパフォーマンスを継続的に監視する必要があります。

パフォーマンスメトリクスの収集

typescript// パフォーマンス監視の実装
const performanceAtom = atom({
  renderCount: 0,
  renderTime: 0,
  memoryUsage: 0,
});

const trackPerformanceAtom = atom(
  null,
  (get, set, componentName: string) => {
    const startTime = performance.now();

    return () => {
      const endTime = performance.now();
      const renderTime = endTime - startTime;

      set(performanceAtom, (prev) => ({
        ...prev,
        renderCount: prev.renderCount + 1,
        renderTime: prev.renderTime + renderTime,
      }));

      // パフォーマンスログの送信
      if (renderTime > 16) {
        // 60fpsの閾値
        console.warn(
          `Slow render detected in ${componentName}: ${renderTime}ms`
        );
      }
    };
  }
);

メモリリークの検出

typescript// メモリリーク検出の実装
const memoryLeakDetectorAtom = atom(null, (get, set) => {
  const interval = setInterval(() => {
    if ('memory' in performance) {
      const memory = (performance as any).memory;
      const usedMemory = memory.usedJSHeapSize;

      // メモリ使用量が急激に増加した場合の警告
      if (usedMemory > 50 * 1024 * 1024) {
        // 50MB
        console.warn(
          'High memory usage detected:',
          usedMemory
        );
      }
    }
  }, 5000);

  return () => clearInterval(interval);
});

デバッグと開発者ツール

Jotai DevTools の活用

typescript// 開発環境でのDevTools設定
import { DevTools } from 'jotai-devtools';

const App = () => {
  return (
    <Provider>
      <TodoApp />
      {process.env.NODE_ENV === 'development' && (
        <DevTools />
      )}
    </Provider>
  );
};

カスタムデバッグ Atom

typescript// デバッグ用のAtom
const debugAtom = atom(
  (get) => {
    // すべてのAtomの状態を収集
    return {
      todos: get(todosAtom),
      user: get(userAtom),
      theme: get(themeAtom),
      timestamp: Date.now(),
    };
  },
  (get, set, action: 'log' | 'reset') => {
    if (action === 'log') {
      const state = get(debugAtom);
      console.log('Current Jotai state:', state);
    } else if (action === 'reset') {
      // すべてのAtomをリセット
      set(todosAtom, []);
      set(userAtom, null);
      set(themeAtom, 'light');
    }
  }
);

チーム開発でのベストプラクティス

Atom の命名規則

typescript// 推奨される命名規則
const userProfileAtom = atom<UserProfile | null>(null);
const userPreferencesAtom = atom<UserPreferences>(
  defaultPreferences
);
const userSettingsAtom =
  atom<UserSettings>(defaultSettings);

// アクションAtomの命名
const updateUserProfileAtom = atom(
  null,
  (get, set, profile: UserProfile) => {
    set(userProfileAtom, profile);
  }
);

const resetUserSettingsAtom = atom(null, (get, set) => {
  set(userSettingsAtom, defaultSettings);
});

Atom の構造化

typescript// 関連するAtomをグループ化
// atoms/user.ts
export const userAtoms = {
  profile: atom<UserProfile | null>(null),
  preferences: atom<UserPreferences>(defaultPreferences),
  settings: atom<UserSettings>(defaultSettings),
};

// atoms/todos.ts
export const todoAtoms = {
  items: atom<Todo[]>([]),
  filters: atom<TodoFilters>(defaultFilters),
  sorting: atom<TodoSorting>(defaultSorting),
};

// atoms/index.ts
export * from './user';
export * from './todos';

まとめ

Redux Toolkit から Jotai への移行は、確実に可能です。ただし、それは単純な置き換えではなく、状態管理の考え方自体を変える必要があります。

移行の成功の鍵は、段階的なアプローチにあります。一度にすべてを変更しようとするのではなく、小さな機能から始めて、徐々に範囲を広げていくことが重要です。

Jotai の最大の魅力は、そのシンプルさと直感性にあります。Redux の複雑な概念を理解する必要がなく、React のuseStateに近い感覚で状態管理ができます。これにより、新しいチームメンバーの学習コストを大幅に削減できます。

また、Jotai の原子ベースのアプローチにより、不要な再レンダリングを防ぎ、パフォーマンスの向上も期待できます。特に大規模なアプリケーションでは、この効果が顕著に現れるでしょう。

移行を検討している開発者の方々には、まず小さなプロジェクトや新機能で Jotai を試してみることをお勧めします。その体験を通じて、Jotai の魅力と可能性を実感していただけると思います。

状態管理の選択は、アプリケーションの将来を左右する重要な決定です。Redux Toolkit が確かに優れたツールであることは間違いありませんが、現代の React 開発においては、Jotai のような軽量で直感的なライブラリがより適している場合があります。

あなたのプロジェクトに最適な状態管理ライブラリを選択し、より良い開発体験を実現してください。

関連リンク