2024年のReact状態管理は大きく成熟し、適切なツールの選択は「勝者を選ぶ」ではなく「状態のカテゴリを理解する」ことが重要になりました。コアライブラリのuseStateとuseReducerはローカルな状態に、エコシステムはグローバルUI状態とサーバー状態に特化したソリューションを提供しています。適切なツールを組み合わせることで、最も保守性の高いアプリケーションを構築できます。
useStateとuseReducer:組み込みのシンプルさ
コンポーネントローカルな状態にはuseStateが最適なデフォルトです。フォーム入力、トグル状態、シンプルなUI状態を効率的に扱います。useReducerは複数のサブ値を持つ状態や複雑な遷移ロジックに有用です。
// シンプルなカウンター
const [count, setCount] = useState(0);
// フォーム状態
const [form, setForm] = useState({ name: "", email: "" });
// 複雑なウィザード処理
function wizardReducer(state, action) {
switch (action.type) {
case "NEXT_STEP":
return { ...state, step: state.step + 1 };
case "PREV_STEP":
return { ...state, step: state.step - 1 };
case "SET_FIELD":
return {
...state,
data: { ...state.data, [action.field]: action.value },
};
case "RESET":
return initialState;
default:
return state;
}
}
| 基準 | useState | useReducer |
|---|---|---|
| 複雑性 | 単純な値 | 複数サブ値 |
| ロジックの配置 | コンポーネント内 | 抽出・テスト可能 |
| 依存関係 | 独立 | 連鎖的な遷移 |
| ボイラープレート | 最小限 | コードは増えるが予測可能 |
useContext:組み込みだが制限あり
useContextはprop drillingを解決しますが、パフォーマンス上の制限があります。コンテキスト値の変更は、変更部分を使用しているかどうかに関わらず、すべてのコンシューマーを再レンダリングします。対策としては、コンテキストを変更頻度で分割し、useMemoで値を安定化します。
// 解決策:変更頻度でコンテキストを分割
const themeValue = useMemo(() => ({ theme, setTheme }), [theme]);
const authValue = useMemo(() => ({ user, setUser }), [user]);
テーマやロケール、認証ステータスなど、変更頻度の低いグローバル状態にはuseContextで十分です。高頻度で更新される状態には、専用ライブラリの方が優れたパフォーマンスを発揮します。
Zustand:最小限のグローバル状態
Zustandは軽量グローバル状態管理のデファクトスタンダードになりました。ストアを作成し、フック経由で状態にアクセスし、直接ミューテーションを実行します。
import { create } from "zustand";
const useCartStore = create((set) => ({
items: [],
total: 0,
addItem: (item) =>
set((state) => ({
items: [...state.items, item],
total: state.total + item.price,
})),
removeItem: (id) =>
set((state) => {
const item = state.items.find((i) => i.id === id);
return {
items: state.items.filter((i) => i.id !== id),
total: state.total - (item?.price || 0),
};
}),
}));
function CartSummary() {
const total = useCartStore((state) => state.total);
return <div>合計: ¥{total}</div>;
}
主な利点:プロバイダーのラッピング不要、セレクターによる自動再レンダリング最適化、persist/devtools/immerなどのミドルウェア、TypeScriptファーストの設計、約1KBのバンドルサイズ。
Jotai:アトミック状態管理
Jotaiは状態をアトム(原子)レベルで管理します。各状態は独立したアトムで、遅延評価され、合成や派生が可能です。
import { atom, useAtom } from "jotai";
const searchQueryAtom = atom("");
const resultsAtom = atom(async (get) => {
const query = get(searchQueryAtom);
if (!query) return [];
const res = await fetch(`/api/search?q=${query}`);
return res.json();
});
function Search() {
const [query, setQuery] = useAtom(searchQueryAtom);
const [results] = useAtom(resultsAtom);
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<ResultsList results={results} />
</div>
);
}
利点:粒度の細かい再レンダリング、遅延評価(消費時のみ計算)、基本的なユースケースではグローバルプロバイダー不要、ツリーシェイカブルな設計。各アトムをSuspenseで独立してサスペンドできるため、並行レンダリングとの親和性が高いです。
TanStack Query:サーバー状態
TanStack Query(旧React Query)はサーバー状態という異なるカテゴリを扱います。サーバー状態は本質的にUI状態とは異なり、非同期であり、サーバーを信頼できる唯一の情報源とします。
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
function ProductPage({ id }) {
const queryClient = useQueryClient();
const { data, isLoading } = useQuery({
queryKey: ["product", id],
queryFn: () => fetch(`/api/products/${id}`).then((r) => r.json()),
staleTime: 5 * 60 * 1000,
});
const mutation = useMutation({
mutationFn: (updated) =>
fetch(`/api/products/${id}`, {
method: "PATCH",
body: JSON.stringify(updated),
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["product", id] });
},
});
if (isLoading) return <Spinner />;
return <ProductDetail product={data} onUpdate={mutation.mutate} />;
}
サーバーデータをグローバルなUIストアに重複して保存するのは誤りです。TanStack Queryはキャッシュ無効化、リクエスト重複排除、バックグラウンド再取得、ページネーション、楽観的更新とロールバックを自動的に処理します。
選定基準
| 状態カテゴリ | 例 | 推奨ツール |
|---|---|---|
| コンポーネントローカル | フォーム入力、トグル、カウンター | useState / useReducer |
| グローバルUI | モーダル、サイドバー、テーマ | Zustand / Jotai |
| サーバー状態 | APIデータ、DB結果 | TanStack Query |
| URL状態 | ルートパラメータ、検索クエリ | ルーターのプリミティブ |
パフォーマンスパターン
再レンダリング範囲の最小化が最も重要です。Zustand(セレクター)やJotai(アトム)は設計上、再レンダリング範囲を最小限に抑えます。変更頻度の高い状態と安定した状態を同じコンテキストに配置しないでください。
React 18では同一イベントハンドラ内の複数状態更新が自動的にバッチングされ、1回の再レンダリングにまとめられます。useMemoとuseCallbackは常に使うのではなく、子コンポーネントへのpropsとしてオブジェクトや関数を渡す箇所で戦略的に使用します。
移行戦略
Reduxなどのモノリシックストアからの移行は並行運用が鍵です。
- サーバー状態から移行(Redux → TanStack Query):これだけでストアの大部分が削減可能
- グローバルUI状態を小さなZustandストアに段階的に置き換え
- アダプターレイヤーで両方のストアを共存させながら移行
- 各モジュールを個別に検証してから完全に切り替え
- React DevTools Profilerで各段階のバンドルサイズとレンダリングパフォーマンスを計測
結論
2024年に単一の「最良の」状態管理ツールは存在しません。コンポーネントローカルにはuseState、グローバルUIにはZustandまたはJotai、サーバーデータにはTanStack Query、URL状態にはルーターの機能を適材適所で使用することで、パフォーマンスが高く保守性に優れたアプリケーションを構築できます。
