React状態管理の深掘り #5 Redux Toolkitとレガシーの文脈

読了 5分

ここまで扱ったZustandJotaiは、比較的新しい軽量なツールです。この記事で扱う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-redux

RTKの中心はcreateSliceです。状態、リデューサー、アクションを1箇所で定義します。

features/counterSlice.js
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つのストアにします。

store.js
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./features/counterSlice";

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
});

そして#3 Zustand#4 Jotaiと違い、ReduxはProviderでツリーを包む必要があります。

main.jsx
import { Provider } from "react-redux";
import { store } from "./store";

createRoot(document.getElementById("root")).render(
  <Provider store={store}>
    <App />
  </Provider>
);

コンポーネントで読み書きする #

状態を読むときはuseSelector、アクションを送るときはuseDispatchを使います。

Counter.jsx
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にもサーバー状態用のツールであるRTK Queryが含まれていて、#2のTanStack Queryと似た役割をします。すでにReduxを深く使うプロジェクトならRTK Queryが自然な選択で、そうでなければTanStack Queryのほうが軽いです。どちらにせよ核心は#1の原則です。サーバー状態は専用ツールに任せ、通常のスライスにはクライアント状態だけを置くことです。

新しいプロジェクトでのReduxの位置づけ #

Redux Toolkitは古いReduxよりはるかに簡潔になりましたが、それでもZustandJotaiよりは通るべき構造が多いです。スライス、ストア構成、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枚の決定フローにまとめ、実際の状況で何を選ぶかを整理します。

X