Redux の使い方 — JavaScriptアプリのための予測可能な状態管理コンテナ
一言でいうと
Reduxは、JavaScriptアプリケーションの状態(state)を単一のストアで一元管理し、予測可能な方法で更新するための状態管理ライブラリです。Action → Reducer → Store という単方向データフローにより、アプリの挙動を一貫させ、デバッグやテストを容易にします。
重要: 現在、Redux公式は素のReduxではなく Redux Toolkit(RTK) の使用を強く推奨しています。本記事ではReduxコアのAPIを解説しますが、新規プロジェクトではRedux Toolkitの利用を検討してください。
どんな時に使う?
- 複数のコンポーネント間で共有する状態が多い場合 — ユーザー認証情報、カート内容、通知など、アプリ全体で参照・更新される状態を一元管理したいとき
- 状態の変更履歴を追跡・デバッグしたい場合 — Redux DevToolsによるタイムトラベルデバッグで、いつ・どのActionで・どう状態が変わったかを可視化できます
- サーバーサイドレンダリング(SSR)やテストで状態を再現したい場合 — 状態がシリアライズ可能な純粋なオブジェクトであるため、サーバーでの初期状態注入やテストでの状態再現が容易です
インストール
# Redux Toolkit(推奨)+ React-Redux
npm install @reduxjs/toolkit react-redux
# yarn
yarn add @reduxjs/toolkit react-redux
# pnpm
pnpm add @reduxjs/toolkit react-redux
# Reduxコアライブラリ単体(v5.0.1)
npm install redux
基本的な使い方
最もシンプルなカウンターの例で、Reduxコアの基本的なデータフローを示します。
import { createStore } from 'redux';
// 注意: createStore は非推奨(deprecated)マークがついていますが、
// 機能的には問題なく動作します。新規プロジェクトでは Redux Toolkit の
// configureStore を使用してください。
// 1. State の型定義
interface CounterState {
value: number;
}
// 2. Action の型定義
type CounterAction =
| { type: 'counter/incremented' }
| { type: 'counter/decremented' }
| { type: 'counter/incrementedByAmount'; payload: number };
// 3. 初期状態
const initialState: CounterState = { value: 0 };
// 4. Reducer — 純粋関数で状態を更新
function counterReducer(
state: CounterState = initialState,
action: CounterAction
): CounterState {
switch (action.type) {
case 'counter/incremented':
return { value: state.value + 1 };
case 'counter/decremented':
return { value: state.value - 1 };
case 'counter/incrementedByAmount':
return { value: state.value + action.payload };
default:
return state;
}
}
// 5. Store の作成
const store = createStore(counterReducer);
// 6. 状態の購読
const unsubscribe = store.subscribe(() => {
console.log('Current state:', store.getState());
});
// 7. Action の dispatch
store.dispatch({ type: 'counter/incremented' });
// Current state: { value: 1 }
store.dispatch({ type: 'counter/incrementedByAmount', payload: 5 });
// Current state: { value: 6 }
store.dispatch({ type: 'counter/decremented' });
// Current state: { value: 5 }
// 8. 購読解除
unsubscribe();
よく使うReduxのAPI
1. createStore — ストアの作成
import { createStore } from 'redux';
interface AppState {
count: number;
}
type AppAction = { type: 'INCREMENT' } | { type: 'RESET' };
const reducer = (state: AppState = { count: 0 }, action: AppAction): AppState => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'RESET':
return { count: 0 };
default:
return state;
}
};
// 第2引数でプリロード状態、第3引数でエンハンサーを渡せる
const store = createStore(reducer, { count: 10 });
console.log(store.getState()); // { count: 10 }
注意: Redux v5 では
createStoreに非推奨マークが付いています。公式は@reduxjs/toolkitのconfigureStoreを推奨しています。
2. combineReducers — 複数のReducerを合成
import { combineReducers, createStore } from 'redux';
// ユーザー関連
interface UserState {
name: string;
loggedIn: boolean;
}
type UserAction = { type: 'LOGIN'; payload: string } | { type: 'LOGOUT' };
const userReducer = (
state: UserState = { name: '', loggedIn: false },
action: UserAction
): UserState => {
switch (action.type) {
case 'LOGIN':
return { name: action.payload, loggedIn: true };
case 'LOGOUT':
return { name: '', loggedIn: false };
default:
return state;
}
};
// 通知関連
interface NotificationState {
messages: string[];
}
type NotificationAction =
| { type: 'ADD_NOTIFICATION'; payload: string }
| { type: 'CLEAR_NOTIFICATIONS' };
const notificationReducer = (
state: NotificationState = { messages: [] },
action: NotificationAction
): NotificationState => {
switch (action.type) {
case 'ADD_NOTIFICATION':
return { messages: [...state.messages, action.payload] };
case 'CLEAR_NOTIFICATIONS':
return { messages: [] };
default:
return state;
}
};
// 合成: state.user / state.notifications としてアクセス可能
const rootReducer = combineReducers({
user: userReducer,
notifications: notificationReducer,
});
type RootState = ReturnType<typeof rootReducer>;
const store = createStore(rootReducer);
store.dispatch({ type: 'LOGIN', payload: 'Tanaka' } as UserAction);
console.log(store.getState().user);
// { name: 'Tanaka', loggedIn: true }
3. applyMiddleware — ミドルウェアの適用
import { createStore, applyMiddleware, Middleware } from 'redux';
// カスタムロガーミドルウェア
const loggerMiddleware: Middleware = (storeAPI) => (next) => (action) => {
console.log('Dispatching:', action);
const result = next(action);
console.log('Next state:', storeAPI.getState());
return result;
};
// 非同期処理用の簡易 thunk ミドルウェア
const thunkMiddleware: Middleware = (storeAPI) => (next) => (action) => {
if (typeof action === 'function') {
return action(storeAPI.dispatch, storeAPI.getState);
}
return next(action);
};
interface AppState {
value: number;
}
type AppAction = { type: 'SET'; payload: number };
const reducer = (state: AppState = { value: 0 }, action: AppAction): AppState => {
switch (action.type) {
case 'SET':
return { value: action.payload };
default:
return state;
}
};
const store = createStore(
reducer,
applyMiddleware(loggerMiddleware, thunkMiddleware)
);
store.dispatch({ type: 'SET', payload: 42 });
// Dispatching: { type: 'SET', payload: 42 }
// Next state: { value: 42 }
4. compose — 関数合成ユーティリティ
import { createStore, applyMiddleware, compose } from 'redux';
// Redux DevTools Extension との連携でよく使われるパターン
const composeEnhancers =
(typeof window !== 'undefined' &&
(window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) ||
compose;
interface AppState {
count: number;
}
type AppAction = { type: 'INC' };
const reducer = (state: AppState = { count: 0 }, action: AppAction): AppState => {
switch (action.type) {
case 'INC':
return { count: state.count + 1 };
default:
return state;
}
};
const loggerMiddleware = (storeAPI: any) => (next: any) => (action: any) => {
console.log('Action:', action.type);
return next(action);
};
const store = createStore(
reducer,
composeEnhancers(applyMiddleware(loggerMiddleware))
);
5. bindActionCreators — Action Creatorをdispatchにバインド
import { createStore, bindActionCreators } from 'redux';
// Action Creators
const increment = () => ({ type: 'INCREMENT' as const });
const decrement = () => ({ type: 'DECREMENT' as const });
const addAmount = (amount: number) => ({
type: 'ADD_AMOUNT' as const,
payload: amount,
});
type AppAction = ReturnType<typeof increment | typeof decrement | typeof addAmount>;
interface AppState {
count: number;
}
const reducer = (state: AppState = { count: 0 }, action: AppAction): AppState => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
case 'ADD_AMOUNT':
return { count: state.count + action.payload };
default:
return state;
}
};
const store = createStore(reducer);
// dispatch を自動的にバインド
const actions = bindActionCreators(
{ increment, decrement, addAmount },
store.dispatch
);
// dispatch を明示的に呼ばなくてよい
actions.increment(); // store.dispatch(increment()) と同等
actions.addAmount(10); // store.dispatch(addAmount(10)) と同等
console.log(store.getState()); // { count: 11 }
類似パッケージとの比較
| 特徴 | Redux (コア) | Redux Toolkit (RTK) | Zustand | MobX | Jotai |
|---|---|---|---|---|---|
| バンドルサイズ | ~2kB | ~11kB | ~1kB | ~16kB | ~3kB |
| ボイラープレート | 多い | 少ない | 非常に少ない | 少ない | 非常に少ない |
| 学習コスト | 中〜高 | 中 | 低 | 中 | 低 |
| DevTools | ✅(拡張機能) | ✅(組み込み) | ✅(Redux DevTools互換) | ✅ | ✅(限定的) |
| ミドルウェア | ✅ | ✅(thunk組み込み) | ✅ | ❌(不要) | ❌(不要) |
| TypeScript対応 | ✅(手動型定義が多い) | ✅(型推論が優秀) | ✅ | ✅ | ✅ |
| 設計思想 | Flux / 関数型 | Flux / 関数型 | Flux風 / シンプル | リアクティブ / OOP | アトミック |
| React依存 | なし | なし | なし | なし | あり |
結論: 新規プロジェクトでReduxを選ぶなら Redux Toolkit 一択です。よりシンプルな状態管理で十分なら Zustand や Jotai も有力な選択肢です。
注意点・Tips
1. createStore の非推奨について
Redux v5 で createStore に視覚的な非推奨マーク(取り消し線)が付きましたが、削除される予定はありません。これは「Redux Toolkitの configureStore を使ってほしい」というシグナルです。
// 非推奨マークを消したい場合は legacy_createStore を使う
import { legacy_createStore as createStore } from 'redux';
2. Reducerでは必ず新しいオブジェクトを返す
// ❌ NG: 直接ミューテーション
const badReducer = (state: AppState, action: any) => {
state.count += 1; // 参照が変わらないので再レンダリングされない
return state;
};
// ✅ OK: 新しいオブジェクトを返す
const goodReducer = (state: AppState, action: any) => {
return { ...state, count: state.count + 1 };
};
3. Redux v5 の破壊的変更に注意
Redux v5.0 では以下の変更があります(v4からの移行時に注意):
- TypeScript 4.7以上が必須
- Action の
typeプロパティがstring型に限定(symbolは不可に) AnyActionが非推奨 →UnknownActionに変更Middlewareの型パラメータが変更されています
4. ストアは原則1つ
// ❌ アンチパターン: 複数ストア
const userStore = createStore(userReducer);
const productStore = createStore(productReducer);
// ✅ 推奨: combineReducers で1つにまとめる
const rootReducer = combineReducers({
user: userReducer,
product: productReducer,
});
const store = createStore(rootReducer);