Redux完全ガイド:基本概念からReactでの使い方までを網羅
序文:なぜ私たちは状態管理に悩むのか?
現代のフロントエンド開発、特にReactを使った開発において、「状態(state)管理」は避けて通れない中心的な課題です。コンポーネントが持つ小さな状態から、アプリケーション全体で共有されるべき広範なデータまで、その管理方法はアプリケーションの品質、保守性、そして開発体験に直結します。
ReactにはuseState
やuseReducer
といった強力な状態管理フックが組み込まれています。しかし、アプリケーションが大規模化・複雑化するにつれて、これらの標準機能だけでは立ち行かなくなる場面が出てきます。例えば、以下のような問題に直面したことはないでしょうか?
- Prop Drilling(プロップのバケツリレー): 深くネストしたコンポーネントに状態を渡すためだけに、中間のコンポーネントが延々とプロップを中継し続ける問題。コードの見通しを悪くし、リファクタリングを困難にします。
- 状態の散在: 関連する状態が複数のコンポーネントに散らばってしまい、どこで何が更新されているのか追跡が困難になる。
- ビジネスロジックの肥大化: UIコンポーネント内にデータ取得や加工などのビジネスロジックが混在し、コンポーネントの責務が曖昧になる。
Reduxは、まさにこれらの課題を解決するために生まれた、JavaScriptアプリケーションのための予測可能な状態コンテナです。Reduxを導入することで、アプリケーションの状態を一元管理し、状態の変更を予測可能で追跡しやすいものにすることができます。
しかし、「Reduxは学習コストが高い」「お決まりのコード(ボイラープレート)が多い」といった声を聞いたことがあるかもしれません。それは過去の話です。現在では、公式推奨のライブラリである Redux Toolkit の登場により、Reduxは驚くほどシンプルかつ効率的に記述できるようになりました。
この記事では、Reduxの根本的な思想から、モダンなRedux Toolkitを使った実践的なReactアプリケーションでの使い方、非同期処理の扱い方、そしてベストプラクティスまで、Reduxに関する全てを網羅的に解説します。あなたがRedux初心者であっても、あるいは一度挫折した経験があっても、この記事を読み終える頃には、自信を持ってReduxをプロジェクトに導入できるようになっているはずです。
第1章: Reduxのコアコンセプト – 思想を理解する
Reduxを効果的に使うためには、まずその根底にある思想、つまり「3つの原則」を理解することが不可欠です。これらの原則が、Reduxの予測可能性と保守性の高さを支えています。
Reduxを支える3つの原則
1. Single source of truth(唯一の情報源)
これは、アプリケーション全体のすべての状態(state)が、store
と呼ばれる一つのオブジェクトツリーに集約されるという原則です。
複数のコンポーネントがそれぞれ独立して状態を持つのではなく、すべての状態が一箇所にまとまっています。これにより、以下のようなメリットが生まれます。
- 信頼性の向上: アプリケーションのどこからでも同じ状態にアクセスできるため、「どの状態が正しいのか?」という混乱がなくなります。
- デバッグの容易さ: アプリケーションの現在の状態を知りたい場合、storeの中身を覗くだけで全体像を把握できます。
- 状態の永続化: storeの状態をシリアライズ(文字列化)してlocalStorageに保存し、次回起動時に復元するといった実装が容易になります。
2. State is read-only(stateは読み取り専用)
これは、stateを直接変更してはならないという原則です。状態を変更する唯一の方法は、action
と呼ばれる、何が起こったかを示すプレーンなオブジェクトをdispatch
(発送)することです。
state.user.name = 'Taro'
のような直接的な書き換えは厳禁です。なぜなら、誰が、いつ、どこで状態を変更したのか追跡できなくなり、アプリケーションの挙動が予測不能になるからです。
状態の変更は、必ず「〇〇がしたい」という意図を表明するaction
を通じて行います。これにより、すべての状態変更が中央集権的に管理され、デバッグやロギングが非常に簡単になります。この原則は、データの流れを一方通行に保ち、アプリケーションの複雑さを軽減します。
3. Changes are made with pure functions(変更は純粋関数で行う)
これは、action
によってどのようにstateが変更されるかを記述するために、reducer
と呼ばれる純粋関数(Pure Function)を使用するという原則です。
Reducerは、現在のstate
と発行されたaction
の2つを引数として受け取り、新しいstateを返す関数です。そのシグネチャは (previousState, action) => newState
となります。
ここで重要なのは、reducerが純粋関数であるという点です。純粋関数とは、以下の2つの条件を満たす関数です。
- 同じ入力に対して、常に同じ出力を返す。
- 副作用(API呼び出し、グローバル変数の変更、引数の直接変更など)を持たない。
Reducerが純粋関数であることにより、stateの更新ロジックは非常に予測可能で、テストも容易になります。引数として渡されたpreviousState
を直接変更するのではなく、コピーを作成して変更を加え、新しいオブジェクトとして返す(イミュータビリティの維持)ことが極めて重要です。
Reduxの主要な登場人物
3つの原則を理解したところで、Reduxを構成する主要な要素を見ていきましょう。
-
Store:
- アプリケーションのstateを保持するオブジェクトです。まさに「唯一の情報源」そのものです。
getState()
: 現在のstateツリーを返します。dispatch(action)
: actionをトリガーし、stateの更新を開始します。これがstateを変更する唯一の方法です。subscribe(listener)
: stateが変更されるたびに呼び出されるコールバック関数を登録します。通常はUIライブラリ(react-redux)が内部で使用します。
-
Action:
- stateの変更内容を記述するプレーンなJavaScriptオブジェクトです。
- 必ず
type
プロパティを持たなければなりません。これは、どのような種類の変更が行われるかを示す文字列です(例:'todos/addTodo'
)。 type
以外のプロパティは自由ですが、慣習的にpayload
というプロパティに変更に必要なデータを含めます。
javascript
// Actionの例
{
type: 'counter/increment',
payload: 1
}
{
type: 'todos/addTodo',
payload: { id: 1, text: 'Learn Redux', completed: false }
}- Action Creator: Actionオブジェクトを生成して返す関数のことです。毎回手でオブジェクトを書く手間を省き、一貫性を保ちます。
javascript
// Action Creatorの例
const addTodo = (text) => {
return {
type: 'todos/addTodo',
payload: { id: Date.now(), text, completed: false }
};
}; -
Reducer:
(previousState, action) => newState
という形式の純粋関数です。- 受け取った
action.type
に応じて、どのようにstateを更新するかを決定します。 switch
文やif
文を使って、action.type
ごとに処理を分岐させるのが一般的です。- 対応しないactionを受け取った場合は、必ず
previousState
をそのまま返さなければなりません。 - イミュータビリティ(不変性) を守ることが非常に重要です。stateを直接変更せず、スプレッド構文 (
...
) や配列のメソッド (.map
,.filter
) などを使って新しいstateオブジェクト/配列を作成して返します。
“`javascript
// Reducerの例
const initialState = { value: 0 };function counterReducer(state = initialState, action) {
switch (action.type) {
case ‘counter/increment’:
// stateを直接変更せず、新しいオブジェクトを返す
return { …state, value: state.value + 1 };
case ‘counter/decrement’:
return { …state, value: state.value – 1 };
default:
// どのactionにも該当しない場合は、元のstateをそのまま返す
return state;
}
}
“`
これらの要素が連携することで、Reduxのデータフローが完成します。
UI → dispatch(action)
→ Reducer → Store → UI
この一方向のデータフローが、Reduxアプリケーションの予測可能性とメンテナンス性の高さを実現しているのです。
第2章: Redux Toolkitを使ったモダンなRedux開発
前章で解説したReduxの基本概念は非常に重要ですが、これらを素のJavaScriptで実装しようとすると、多くの「お決まりのコード(ボイラープレート)」が必要でした。Action Typeの文字列定義、Action Creator関数、Reducer内のswitch
文、Storeの設定… これらが開発者の負担となり、「Reduxは複雑だ」という印象を与える一因でした。
この問題を解決するために、Reduxチームが公式に開発し、推奨しているのが Redux Toolkit (RTK) です。RTKは、Reduxを使った開発を効率化し、ベストプラクティスを強制するためのツールセットです。
なぜRedux Toolkitを使うべきか?
- 設定の簡略化: ストアの作成やミドルウェア(後述)の設定が非常に簡単になります。
- ボイラープレートの削減: ActionやReducerの定義を劇的に簡潔に記述できます。
- ベストプラクティスの導入: イミュータビリティの強制(Immerライブラリの内部利用)や、非同期処理の標準的なパターンの提供など、良い設計が自然とできるようになっています。
これ以降の解説は、すべてRedux Toolkitを前提として進めます。
configureStore
: ストア設定の簡略化
従来のcreateStore
に代わる、RTKの中心的なAPIです。ストアの作成と設定を一つの関数呼び出しで行えます。
“`javascript
// app/store.js
import { configureStore } from ‘@reduxjs/toolkit’;
import counterReducer from ‘../features/counter/counterSlice’;
export const store = configureStore({
reducer: {
// ここに各機能のReducerを登録していく
counter: counterReducer,
// todos: todosReducer,
// …
},
});
“`
configureStore
は内部的に以下のことを自動で行ってくれます。
- 渡された複数のreducerを
combineReducers
を使って一つにまとめる。 - Redux Thunkミドルウェア(非同期処理で使います)をデフォルトで追加する。
- Redux DevTools Extensionとの連携を自動で有効にする。
もはや、ストアの設定で悩む必要はありません。
createSlice
: ActionとReducerをまとめて定義
createSlice
は、RTKの最も革新的な機能の一つです。これまでバラバラに定義していたAction Type、Action Creator、Reducerのロジックを、「スライス(slice)」 という単位で一箇所にまとめて定義できます。スライスとは、state全体の中の一部分(機能ごと)と、それに関連するロジックのまとまりを指します。
“`javascript
// features/counter/counterSlice.js
import { createSlice } from ‘@reduxjs/toolkit’;
const initialState = {
value: 0,
status: ‘idle’,
};
export const counterSlice = createSlice({
name: ‘counter’, // スライスの名前。action typeのプレフィックスになる
initialState, // このスライスの初期状態
// Reducerロジックを記述するオブジェクト
reducers: {
// 各キーがAction Creatorの名前になる
increment: (state) => {
// Immerが内部で動いているため、”ミュータブル”なコードが書ける!
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
// payloadを受け取る場合は第2引数で受け取る
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
});
// Action Creatorが自動でエクスポートされる
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
// Reducerがエクスポートされる (storeで登録するため)
export default counterSlice.reducer;
“`
createSlice
の驚くべき点は以下の通りです。
- ActionとReducerの自動生成:
reducers
オブジェクトにメソッドを定義するだけで、対応するAction Creator (counterSlice.actions
)とReducerロジックが自動的に生成されます。'counter/increment'
のようなAction Type文字列を自分で書く必要はもうありません。 - Immerによるイミュータブルな更新:
reducers
内の関数では、state
を直接変更しているように見えます(例:state.value += 1
)。しかし、これはRTKが内部でImmerというライブラリを使用しているためです。Immerは、このような「ミュータブルに見えるコード」を検知し、裏側で安全なイミュータブルな更新処理に変換してくれます。これにより、開発者はスプレッド構文のネスト地獄から解放され、直感的にstateを更新できます。
createSlice
の登場により、Reduxのボイラープレート問題はほぼ解決されたと言えるでしょう。
第3章: ReactとReduxを連携させる (react-redux)
ReduxはReactから独立したライブラリですが、Reactアプリケーションで使うのが一般的です。その両者を繋ぐための公式ライブラリが react-redux
です。react-redux
は、ReactコンポーネントがReduxストアと効率的にやり取りするためのフックやコンポーネントを提供します。
<Provider>
コンポーネント
まず、アプリケーション全体でReduxストアを利用できるようにするために、Provider
コンポーネントでアプリケーションのルートコンポーネントをラップします。store
プロップに、configureStore
で作成したストアを渡します。
“`jsx
// src/index.js (または main.jsx)
import React from ‘react’;
import ReactDOM from ‘react-dom/client’;
import App from ‘./App’;
import { store } from ‘./app/store’; // 作成したストアをインポート
import { Provider } from ‘react-redux’; // Providerをインポート
const root = ReactDOM.createRoot(document.getElementById(‘root’));
root.render(
{/ ProviderでAppをラップし、storeを渡す /}
);
“`
これにより、App
コンポーネント以下のすべてのコンポーネントからReduxストアにアクセスできるようになります。
useSelector
フック: ストアからデータを読み取る
コンポーネント内でReduxストアのstateを参照するには、useSelector
フックを使います。このフックは引数としてセレクター関数を受け取ります。セレクター関数は、state
全体を引数に取り、その中からコンポーネントが必要とするデータを取り出して返す関数です。
“`jsx
import { useSelector } from ‘react-redux’;
// …コンポーネント内
// state全体から counter スライスの value を選択する
const count = useSelector((state) => state.counter.value);
“`
useSelector
は、セレクター関数が返す値が変化した時のみ、コンポーネントを再レンダリングします。これにより、関係ないstateの変更による不要な再レンダリングを防ぎ、パフォーマンスを最適化します。
useDispatch
フック: Actionをディスパッチする
コンポーネントからstateを変更する(Actionを発行する)には、useDispatch
フックを使います。このフックは、ストアのdispatch
関数を返します。
“`jsx
import { useDispatch } from ‘react-redux’;
import { increment, decrement } from ‘./counterSlice’; // Action Creatorをインポート
// …コンポーネント内
const dispatch = useDispatch();
// …
“`
dispatch
関数に、createSlice
からエクスポートしたAction Creator(increment()
など)を渡すことで、対応するActionがディスパッチされ、Reducerが実行されてstateが更新されます。
実践例: カウンターアプリの実装
ここまでの知識を総動員して、簡単なカウンターアプリを作ってみましょう。
-
ファイル構成
src/
├── app/
│ └── store.js # Reduxストアの設定
├── features/
│ └── counter/
│ ├── Counter.jsx # Reactコンポーネント
│ └── counterSlice.js # Reduxスライス
├── App.jsx
└── index.js -
counterSlice.js
(createSlice
を使用)“`javascript
// src/features/counter/counterSlice.js
import { createSlice } from ‘@reduxjs/toolkit’;export 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;
“` -
store.js
(configureStore
を使用)“`javascript
// src/app/store.js
import { configureStore } from ‘@reduxjs/toolkit’;
import counterReducer from ‘../features/counter/counterSlice’;export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
“` -
index.js
(Provider
でラップ) – 前述の通り -
Counter.jsx
(useSelector
とuseDispatch
を使用)“`jsx
// src/features/counter/Counter.jsx
import React, { useState } from ‘react’;
import { useSelector, useDispatch } from ‘react-redux’;
import { increment, decrement, incrementByAmount } from ‘./counterSlice’;export function Counter() {
// ストアから現在のカウンターの値を取得
const count = useSelector((state) => state.counter.value);
// dispatch関数を取得
const dispatch = useDispatch();
// コンポーネント自身のローカルstate
const [incrementAmount, setIncrementAmount] = useState(‘2’);const addValue = Number(incrementAmount) || 0;
return (
{count}
setIncrementAmount(e.target.value)}
/>
);
}
“` -
App.jsx
“`jsx
// src/App.jsx
import { Counter } from ‘./features/counter/Counter’;function App() {
return (
);
}export default App;
“`
これで、ReactとReduxが連携したカウンターアプリが完成しました。コンポーネントはUIの表示とユーザー操作の検知に集中し、状態管理のロジックはReduxスライスに完全に分離されています。これがReduxがもたらす関心の分離の力です。
第4章: Reduxにおける非同期処理
現実のアプリケーションでは、APIからデータを取得するなどの非同期処理が必須です。しかし、ReduxのReducerは純粋関数でなければならず、副作用(APIリクエストなど)を持つことはできません。
この問題を解決するのが ミドルウェア です。ミドルウェアは、dispatch
されたActionがReducerに到達するまでの間に介在し、追加の処理を行うための仕組みです。非同期処理を行うミドルウェアは、特定の種類のAction(例えば、オブジェクトではなく関数)を検知し、APIリクエストを実行し、その結果に基づいて新しいActionをdispatch
します。
Redux ThunkとcreateAsyncThunk
最も広く使われている非同期処理ミドルウェアが Redux Thunk です。Thunkは、Actionの代わりに「関数」をdispatch
できるようにするミドルウェアです。この関数は引数としてdispatch
とgetState
を受け取るため、その中で非同期処理を行い、結果に応じてActionをdispatch
できます。
幸いなことに、Redux ToolkitのconfigureStore
は、デフォルトでRedux Thunkを有効にしています。さらに、RTKはThunkを使った非同期処理の典型的なパターンをカプセル化した createAsyncThunk
というAPIを提供しており、これを使うのが現代の標準的な方法です。
createAsyncThunk
は、以下の3つの引数を取ります。
- Action Typeのプレフィックス:
'users/fetchUsers'
のように、非同期処理の内容を示す文字列。 - Payload Creator:
Promise
を返す非同期関数。APIリクエストなどの実際の処理はここに書きます。 - オプションオブジェクト(省略可能)
createAsyncThunk
は、内部でこの非同期関数を実行し、そのPromiseの状態(pending
, fulfilled
, rejected
)に応じて、自動的にActionをdispatch
してくれます。
pending
: 非同期処理が開始された時 (users/fetchUsers/pending
)fulfilled
: 非同期処理が成功した時 (users/fetchUsers/fulfilled
)。戻り値がaction.payload
になる。rejected
: 非同期処理が失敗した時 (users/fetchUsers/rejected
)。エラー情報がaction.error
に入る。
これらの自動生成されたActionは、createSlice
の extraReducers
というオプションでハンドリングします。reducers
オプションはスライス内部のActionのみを扱いますが、extraReducers
はスライス外部で定義されたAction(createAsyncThunk
が生成するものなど)を扱うために使います。
実践例: 非同期でTodoリストを取得する
“`javascript
// features/todos/todosSlice.js
import { createSlice, createAsyncThunk } from ‘@reduxjs/toolkit’;
import axios from ‘axios’; // APIリクエスト用ライブラリ
// createAsyncThunkで非同期処理を定義
export const fetchTodos = createAsyncThunk(
‘todos/fetchTodos’, // 1. Action typeのプレフィックス
async () => { // 2. Payload Creator (Promiseを返す非同期関数)
const response = await axios.get(‘https://jsonplaceholder.typicode.com/todos?_limit=5’);
return response.data; // ここで返された値が fulfilled action の payload になる
}
);
const todosSlice = createSlice({
name: ‘todos’,
initialState: {
entities: [],
loading: ‘idle’, // ‘idle’ | ‘pending’ | ‘succeeded’ | ‘failed’
error: null,
},
reducers: {
// 同期的なActionはここに書く (例: todoの追加など)
},
// 非同期的なActionのハンドリングは extraReducers で行う
extraReducers: (builder) => {
builder
.addCase(fetchTodos.pending, (state) => {
state.loading = ‘pending’;
})
.addCase(fetchTodos.fulfilled, (state, action) => {
state.loading = ‘succeeded’;
state.entities = action.payload; // APIから取得したデータをstateに格納
})
.addCase(fetchTodos.rejected, (state, action) => {
state.loading = ‘failed’;
state.error = action.error.message;
});
},
});
export default todosSlice.reducer;
“`
Reactコンポーネント側では、この非同期Thunkをdispatch
し、loading
状態に応じてUIを切り替えます。
“`jsx
// features/todos/TodoList.jsx
import React, { useEffect } from ‘react’;
import { useSelector, useDispatch } from ‘react-redux’;
import { fetchTodos } from ‘./todosSlice’;
export function TodoList() {
const dispatch = useDispatch();
const todos = useSelector((state) => state.todos.entities);
const loadingStatus = useSelector((state) => state.todos.loading);
const error = useSelector((state) => state.todos.error);
useEffect(() => {
// コンポーネントのマウント時に一度だけデータを取得する
if (loadingStatus === ‘idle’) {
dispatch(fetchTodos());
}
}, [loadingStatus, dispatch]);
if (loadingStatus === ‘pending’) {
return
;
}
if (loadingStatus === ‘failed’) {
return
;
}
return (
-
{todos.map((todo) => (
- {todo.title}
))}
);
}
“`
このように、createAsyncThunk
とextraReducers
を組み合わせることで、ローディング状態やエラーハンドリングを含む非同期処理の定型的なロジックを、非常にクリーンかつ宣言的に記述することができます。
第5章: Reduxのベストプラクティスと応用
Reduxを使いこなすためには、いくつかのベストプラクティスと応用的なテクニックを知っておくと良いでしょう。
ファイル構成: Feature-Sliced Design
アプリケーションが大きくなるにつれて、ファイル構成が重要になります。Reduxでは、機能ごとに関連ファイルをまとめる“Feature Sliced”アプローチが推奨されています。
src/
├── app/ # アプリ全体の設定 (store, etc.)
│ └── store.js
├── features/ # 各機能ごとのディレクトリ
│ ├── counter/
│ │ ├── Counter.jsx
│ │ └── counterSlice.js
│ ├── todos/
│ │ ├── TodoList.jsx
│ │ ├── AddTodo.jsx
│ │ └── todosSlice.js
│ └── ...
├── components/ # 複数の機能で共有されるUIコンポーネント
│ └── Button.jsx
└── ...
この構成により、各機能が自己完結し、見通しが良く、再利用や削除が容易になります。
Stateの正規化: createEntityAdapter
APIから取得するデータは、しばしばネストした構造になっています。例えば、ブログ記事とそのコメントのようなデータです。このようなデータを配列のままstateで管理すると、特定の要素の更新や検索が非効率になります。
そこで正規化(Normalization)というテクニックが役立ちます。これは、データベースのテーブルのように、データをIDベースのルックアップテーブル形式で保存する手法です。
“`javascript
// 正規化されていないstate
{
posts: [
{ id: ‘post1’, title: ‘First Post’, comments: [‘comment1’, ‘comment2’] },
{ id: ‘post2’, title: ‘Second Post’, comments: [‘comment3’] }
],
comments: [
{ id: ‘comment1’, text: ‘…’ },
{ id: ‘comment2’, text: ‘…’ },
{ id: ‘comment3’, text: ‘…’ },
]
}
// 正規化されたstate
{
posts: {
ids: [‘post1’, ‘post2’], // IDの配列で順序を保持
entities: { // IDをキーとするオブジェクトで実体を保持
‘post1’: { id: ‘post1’, title: ‘First Post’, comments: [‘comment1’, ‘comment2’] },
‘post2’: { id: ‘post2’, title: ‘Second Post’, comments: [‘comment3’] }
}
},
comments: {
ids: [‘comment1’, ‘comment2’, ‘comment3’],
entities: {
‘comment1’: { id: ‘comment1’, text: ‘…’ },
‘comment2’: { id: ‘comment2’, text: ‘…’ },
‘comment3’: { id: ‘comment3’, text: ‘…’ },
}
}
}
“`
正規化されたstateは、特定のIDを持つエンティティ(例: state.posts.entities['post1']
)へのアクセスがO(1)となり、非常に高速です。また、データの重複がなくなり、更新も容易になります。
この正規化処理を簡単に行うために、Redux Toolkitは createEntityAdapter
というユーティリティを提供しています。これは、正規化されたstate構造を管理するためのreducerロジックやセレクターを自動生成してくれる強力なツールです。
セレクターのメモ化: Reselect
useSelector
は非常に効率的ですが、セレクター関数内でフィルタリングやマッピングなどの重い計算を行う場合、関係ないstateの変更によってもその計算が再実行されてしまう可能性があります。
Reselectは、メモ化(memoization)されたセレクターを作成するためのライブラリです(RTKにも同梱されています)。createSelector
関数は、入力となるセレクターとその結果を計算する関数を受け取ります。入力セレクターが返す値が前回と同じであれば、計算関数を再実行せず、キャッシュしておいた前回の結果を返します。
“`javascript
import { createSelector } from ‘@reduxjs/toolkit’;
const selectTodos = (state) => state.todos.entities;
const selectFilter = (state) => state.visibilityFilter;
// メモ化されたセレクターを作成
export const selectVisibleTodos = createSelector(
[selectTodos, selectFilter], // 1. 入力セレクターの配列
(todos, filter) => { // 2. 結果を計算する関数
// この部分は、todosかfilterが変更された時だけ実行される
switch (filter) {
case ‘SHOW_COMPLETED’:
return todos.filter(t => t.completed);
case ‘SHOW_ACTIVE’:
return todos.filter(t => !t.completed);
default:
return todos;
}
}
);
// コンポーネントでの使用
const visibleTodos = useSelector(selectVisibleTodos);
“`
これにより、不要な再計算を防ぎ、アプリケーションのパフォーマンスをさらに向上させることができます。
第6章: Reduxはもう古い?Context APIや他のライブラリとの比較
「Reduxの代わりにReactのContext APIを使えばいいのでは?」という議論はよく目にします。また、ZustandやJotaiといった新しい状態管理ライブラリも登場しています。Reduxは今でも最適な選択肢なのでしょうか?
Redux vs React Context API
- Context API: Reactに組み込まれた、Prop Drillingを回避するための機能です。
Provider
で提供した値を、ツリー内の任意の子孫コンポーネントがuseContext
フックで直接受け取ることができます。 - 使い分け:
- Context APIが適しているケース:
- 更新頻度が低いグローバルなデータ(例: テーマ設定、ユーザー認証情報、言語設定)。
- 小〜中規模のアプリケーションで、Reduxの導入コストを避けたい場合。
- Reduxが適しているケース:
- アプリケーションの多くの部分に影響を与える、更新頻度が高い複雑な状態。
- 状態の変更履歴を追跡したい、タイムトラベルデバッグを行いたいなど、Redux DevToolsの強力なデバッグ機能が必要な場合。
- 非同期処理やミドルウェアによる副作用の管理を、一貫したパターンで行いたい大規模なアプリケーション。
- Context APIが適しているケース:
パフォーマンスに関する注意点: Context APIは、Provider
から提供される値が変更されると、そのContextをuseContext
で購読しているすべてのコンポーネントが再レンダリングされる傾向があります。一方、react-redux
のuseSelector
は、セレクターが返す値が実際に変更された場合のみコンポーネントを再レンダリングするため、より細やかなパフォーマンス最適化が可能です。
その他の状態管理ライブラリ
- Zustand: Reduxにインスパイアされつつも、フックベースで非常にシンプルに書けるライブラリ。ボイラープレートがほとんどなく、Redux Toolkitよりもさらに手軽に始められます。
- Jotai / Recoil: 「アトム」という小さな状態の単位をベースにするアトミックな状態管理ライブラリ。
useState
のような感覚でグローバルな状態を扱えるのが特徴です。
これらのライブラリは、特定のユースケースにおいてReduxよりも優れた選択肢となることがあります。特に小〜中規模のプロジェクトや、よりシンプルなAPIを好む場合に人気があります。
結論: いつReduxを選ぶべきか?
Reduxは決して「古い」技術ではありません。Redux Toolkitによって開発体験が大幅に改善され、今なお大規模で複雑なWebアプリケーションを構築するための最も堅牢で信頼性の高い選択肢の一つです。
Reduxを選ぶべき主な理由:
- 予測可能性と保守性: 3つの原則と一方向データフローにより、大規模なコードベースでも状態の変更が追跡しやすくなります。
- 強力なエコシステムとツール: Redux DevToolsは他の追随を許さないデバッグ体験を提供します。また、豊富なミドルウェアやライブラリが存在します。
- チーム開発でのスケーラビリティ: 明確なルールと構造があるため、多くの開発者が関わるプロジェクトでも一貫性を保ちやすいです。
- 実績と安定性: 長年にわたり多くのプロダクション環境で使われてきた実績があります。
プロジェクトの要件、チームのスキルセット、アプリケーションの規模を考慮して、最適な状態管理ソリューションを選択することが重要です。しかし、複雑な状態管理が要求される場面において、Redux(特にRedux Toolkit)が提供する価値は依然として非常に大きいと言えるでしょう。
まとめ
本記事では、Reduxの基本概念からRedux Toolkitを使ったモダンな開発手法、Reactとの連携、非同期処理、そして応用的なベストプラクティスまで、幅広く掘り下げてきました。
- 3つの原則(唯一の情報源, stateは読み取り専用, 変更は純粋関数で)がReduxの予測可能性の根幹をなすこと。
- Redux Toolkitが、
createSlice
やconfigureStore
によってボイラープレートを劇的に削減し、開発体験を向上させること。 - react-reduxの
Provider
,useSelector
,useDispatch
が、ReactとReduxをシームレスに繋ぐこと。 createAsyncThunk
が、非同期処理のロジックを宣言的かつクリーンに記述する標準的な方法であること。- ファイル構成、stateの正規化、セレクターのメモ化といったベストプラクティスが、アプリケーションをより堅牢でパフォーマンスの高いものにすること。
Reduxは確かに学習すべき概念が多いですが、その投資は、複雑なアプリケーションを自信を持って構築・維持していくための強力な力となります。この記事が、あなたのReduxマスターへの道を照らす一助となれば幸いです。さあ、予測可能な状態管理の世界へ飛び込みましょう!