React状態管理の深掘り #5 Redux Toolkitとレガシーの文脈
ここまで扱ったZustandとJotaiは、比較的新しい軽量なツールです。この記事で扱うReduxは、その前の時代を支配したツールです。しばらく「Reactのグローバル状態 = Redux」が事実上の公式で、その時代に作られた数多くのコードベースに今も残っています。なので、新しく書く機会が減ったとしても、読んで保守する機会は依然として多いです。
この記事では、古いReduxの冗長なコードではなく、現在の公式推奨方式であるRedux Toolkit(RTK)基準で整理します。
Reduxの核心概念 #
Reduxは3つのルールの上に立っています。このモデル自体は、Reactの組み込みuseReducerと同じ根を持ちます。
- 単一ストア — アプリの状態が1つの大きなオブジェクトに集まります。
- アクションでのみ変更 — 状態を直接変えず、「何が起きたか」を記述したアクションをディスパッチします。
- リデューサーが次の状態を計算 —
(現在の状態, アクション) => 次の状態という純粋関数が変更を担います。
この厳格さがReduxの長所であり、コストでもあります。状態の変化が常にアクションを通るので、追跡とデバッグ(タイムトラベルデバッグを含む)が強力です。代わりに、その分だけ通るべき段階が多いです。
古いReduxが冗長だった理由 #
かつてのReduxは、機能を1つ追加するのに、アクションタイプ定数、アクション生成関数、リデューサーを、それぞれ別のファイルに手で書く必要がありました。カウンター1つに数十行かかり、このボイラープレートこそがReduxから人々が離れた最大の理由でした。Redux Toolkitは、まさにこの問題を解くために公式チームが作ったツールです。
インストールとスライス #
npm install @reduxjs/toolkit react-reduxRTKの中心はcreateSliceです。状態、リデューサー、アクションを1箇所で定義します。
import { createSlice } from "@reduxjs/toolkit";
const counterSlice = createSlice({
name: "counter",
initialState: { value: 0 },
reducers: {
increment: (state) => {
state.value += 1; // 直接変えているように見えるが安全(下で説明)
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;state.value += 1のように、状態を直接変えるかのようなコードが見えます。本来リデューサーは状態を不変に扱うべきですが、RTKは内部にImmerを置き、この「変えるかのような」コードを安全な不変更新に変換します。おかげでスプレッド演算子でオブジェクトをコピーする手間が消えます。そしてリデューサー名(incrementなど)に合うアクション生成関数が自動的に作られます。 古いReduxのボイラープレートの大半がここで消えます。
ストアの構成とProvider #
複数のスライスをまとめて1つのストアにします。
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./features/counterSlice";
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});そして#3 Zustand、#4 Jotaiと違い、ReduxはProviderでツリーを包む必要があります。
import { Provider } from "react-redux";
import { store } from "./store";
createRoot(document.getElementById("root")).render(
<Provider store={store}>
<App />
</Provider>
);コンポーネントで読み書きする #
状態を読むときはuseSelector、アクションを送るときはuseDispatchを使います。
import { useSelector, useDispatch } from "react-redux";
import { increment, decrement, incrementByAmount } from "./features/counterSlice";
function Counter() {
const value = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<button onClick={() => dispatch(decrement())}>-</button>
<span>{value}</span>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(incrementByAmount(5))}>+5</button>
</div>
);
}useSelectorのセレクターは、#3 Zustandのセレクターと同じ発想です。返した値が変わるときだけ、コンポーネントが再レンダリングされます。アクションは必ずdispatchを通るので、状態がいつなぜ変わったかが、Redux DevToolsにアクション単位で記録されます。この追跡性が、大きなチームと複雑な状態でReduxが今も選ばれる理由です。
新しいプロジェクトでのReduxの位置づけ #
Redux Toolkitは古いReduxよりはるかに簡潔になりましたが、それでもZustandやJotaiよりは通るべき構造が多いです。スライス、ストア構成、Providerが標準で必要です。なのでツールを選ぶとき、次のように整理できます。
| 状況 | 適合度 |
|---|---|
| すでにReduxで書かれたコードベースの保守 | RTKが自然な現代化の経路 |
| 大きなチーム、厳格な状態変更の規約と追跡性が重要 | Reduxの強みが生きる |
| 小さなアプリ、軽量なグローバル状態だけが必要 | Zustand・Jotaiのほうが軽い |
| 状態がほとんどサーバーデータ | TanStack Query中心、グローバルストアは最小化 |
つまり新しく始める小さなプロジェクトなら、あえてReduxを選ぶ理由は減りましたが、現場の多くのコードがReduxで書かれているので、読んで扱えることは依然として重要です。 この記事を「レガシーの文脈」と呼んだ理由です。
まとめ #
Redux Toolkitは、古いReduxのボイラープレートを取り払った公式推奨形です。
createSlice— 状態、リデューサー、アクションを1箇所に。Immerで「変えるかのような」安全な更新configureStore— スライスをまとめた単一ストアProvider— Reduxはツリーを包む必要がある(Zustand・Jotaiとの違い)useSelector/useDispatch— 読みとアクションのディスパッチ- 強みは追跡性と規約、コストは構造の重さ
これで、組み込みツールからTanStack Query、Zustand、Jotai、Redux Toolkitまで主要な選択肢をすべて見ました。最終回「React状態管理の深掘り #6 どのツールをいつ使うか — 決定ガイド」では、これらすべてを1枚の決定フローにまとめ、実際の状況で何を選ぶかを整理します。