reduxを理解する
code:redux.ts
📦 1. Store(ストア)の作成
/src/index.tsx:11
const store = createStore(counterReducer, initialState);
Storeとは:アプリ全体の状態(State)を保管する「中央倉庫」です。この例ではcountの値を管理しています。
🎯 2. 初期State(状態)の定義
/src/reducer.ts:4-5
export type CounterState = { count: number };
export const initialState: CounterState = { count: 0 };
最初はcount: 0から始まります。
🎬 3. Action(アクション)の定義
/src/actions.ts:14-26
export const increment = (): CounterAction => ({
type: CounterActionType.INCREMENT,
});
Actionは「何をしたいか」を表すオブジェクト。INCREMENTは「+1したい」という意図を表現。
🖱️ 4. Viewでユーザー操作を受け取る
/src/components/organisms/CounterBoard.tsx:29-31
<Button color="green" onClick={increment}>
+1
</Button>
ユーザーが「+1」ボタンをクリック。
📤 5. Dispatch(ディスパッチ)でActionを送信
/src/containers/organisms/CounterBoard.tsx:17
increment={() => dispatch(increment())}
dispatchは「配送員」のような役割。Actionを包んでReducerに届けます。
🔄 6. Reducer(リデューサー)でStateを更新
/src/reducer.ts:22-26
case Type.INCREMENT:
return {
...state,
count: state.count + 1, // 新しいStateを作成
};
Reducerは「現在のState」と「Action」を受け取って、新しいStateを作ります(元のStateは変更しない)。
📥 7. StoreがStateを保存
Reducerが返した新しいStateがStoreに保存されます。
🔄 8. Viewが自動更新
/src/containers/organisms/CounterBoard.tsx:9
const count = useSelector<CounterState, number>((state) => state.count);
useSelectorでStoreの値を監視。Stateが変わると自動的に画面が再描画されます。
🎯 全体の流れをまとめると
1. ユーザーが「+1」ボタンをクリック
↓
2. increment()アクションが作成される
↓
3. dispatch(increment())でアクションが送信される
↓
4. ReducerがActionを受け取り、新しいStateを計算
↓
5. StoreがStateを更新して保存
↓
6. useSelectorが変化を検知して画面を再描画
↓
7. 画面のcountが0→1に変わる
🏪 Storeはどこ?
/src/index.tsx:11のconst store = createStore(...)がStore本体です。
<Provider store={store}>でアプリ全体にStoreを提供しています。
code:toolkit.ts
Redux Toolkitの一連の流れを説明します(通常のReduxとの違いも含めて)
📦 1. Store(ストア)の作成
/src/index.tsx:11
const store = configureStore({ reducer: counterSlice.reducer });
違い:createStore→configureStoreに。Redux DevToolsが自動設定される。
🎯 2. Slice(スライス)で全部まとめて定義
/src/features/counter.ts:6-17
export const counterSlice = createSlice({
name: 'counter',
initialState: { count: 0 }, // 初期State
reducers: { // ReducerとActionを同時に定義
incremented: (state) => ({ ...state, count: state.count + 1 }),
},
});
大きな違い:
- 通常のRedux:Action、ActionType、Reducerを別々に定義
- Redux Toolkit:createSliceで全部まとめて定義!
🎬 3. Action(アクション)の自動生成
/src/containers/organisms/CounterBoard.tsx:10
const { added, decremented, incremented } = counterSlice.actions;
createSliceが自動でActionクリエイターを生成。手動で書く必要なし!
🖱️ 4. Viewでユーザー操作を受け取る
/src/components/organisms/CounterBoard.tsx:29-31
<Button color="green" onClick={increment}>
+1
</Button>
この部分は通常のReduxと同じ。
📤 5. Dispatch(ディスパッチ)でActionを送信
/src/containers/organisms/CounterBoard.tsx:17
increment={() => dispatch(incremented())}
incremented()は自動生成されたActionクリエイター。
🔄 6. Reducer(リデューサー)でStateを更新
/src/features/counter.ts:15
incremented: (state) => ({ ...state, count: state.count + 1 })
Immerを使えば直接変更もOK:
incremented: (state) => {
state.count += 1; // 直接変更できる!
}
📥 7. StoreがStateを保存
configureStoreが内部で処理。Redux DevToolsで確認可能。
🔄 8. Viewが自動更新
/src/containers/organisms/CounterBoard.tsx:8
const count = useSelector<CounterState, number>((state) => state.count);
通常のReduxと同じくuseSelectorで監視。
🎯 Redux Toolkitの流れをまとめると
1. ユーザーが「+1」ボタンをクリック
↓
2. incremented()が呼ばれる(自動生成されたAction)
↓
3. dispatch(incremented())でActionが送信される
↓
4. createSliceで定義したReducerが実行される
↓
5. configureStoreが新しいStateを保存
↓
6. useSelectorが変化を検知して画面を再描画
↓
7. 画面のcountが0→1に変わる
🆚 通常のReduxとの大きな違い
通常のRedux(02-redux)
- 3つのファイルが必要:actions.ts、reducer.ts、index.tsx
- 型パズルが必要:ValueOf<T>など
- Stateを直接変更できない
Redux Toolkit(04-toolkit)
- 1つのファイルで完結:counter.tsのcreateSlice
- 型は自動推論
- Immerで直接変更可能
- Redux DevToolsが自動設定
コード量が約1/3に削減され、型安全性も向上!
code:user-reducer.ts
useReducerフックの一連の流れを説明します(Reduxとの違いも含めて)
🏠 最大の違い:Storeがない!
/src/index.tsx:8
ReactDOM.render(<App />, document.getElementById('root') as HTMLElement);
Providerも不要!useReducerはコンポーネント内で完結する状態管理。
📦 1. コンポーネント内でuseReducerを使用
/src/containers/templates/CounterWidget.tsx:60-64
const state, dispatch = useReducer(
counterReducer, // Reducer関数
initialCount, // 初期値
(count: number) => ({ count }) // 初期化関数
);
Storeの代わりにuseReducerが状態を管理。各コンポーネントが独自のstateを持つ。
🎯 2. Reducer(リデューサー)の定義
/src/containers/templates/CounterWidget.tsx:18-44
const counterReducer = (
state: CounterState,
action: CounterAction,
): CounterState => {
switch (action.type) {
case CounterActionType.incremented:
return { ...state, count: state.count + 1 };
// ...
}
};
Reduxと同じ形式のReducer。純粋関数で新しいstateを返す。
🎬 3. Action(アクション)の定義
/src/containers/templates/CounterWidget.tsx:53-55
const increment = (): CounterAction => ({
type: CounterActionType.incremented,
});
Reduxと同じAction creator。
🖱️ 4. Viewでユーザー操作を受け取る
/src/components/templates/CounterWidget.tsx:14でCounterBoardに関数を渡す
📤 5. dispatchでActionを送信
/src/containers/templates/CounterWidget.tsx:71
increment={() => dispatch(increment())}
useReducerから返されたdispatchを使用。Reduxのdispatchと同じ使い方。
🔄 6. ReducerでStateを更新
dispatchされたActionに応じてReducerが新しいstateを計算。
🔄 7. コンポーネントが再レンダリング
stateが更新されると、そのコンポーネントだけが再レンダリング。
🎯 useReducerの流れをまとめると
1. ユーザーが「+1」ボタンをクリック
↓
2. increment()アクションが作成される
↓
3. dispatch(increment())でアクションが送信される
↓
4. counterReducerが実行され新しいstateを返す
↓
5. useReducerがstateを更新
↓
6. コンポーネントが再レンダリング
↓
7. 画面のcountが0→1に変わる
🆚 Redux vs useReducer
Redux(グローバル状態管理)
- Store:アプリ全体で1つ
- Provider:必要
- 状態の共有:どのコンポーネントからもアクセス可能
- 使用場面:複数コンポーネント間で状態を共有
useReducer(ローカル状態管理)
- Store:なし(各コンポーネントが独自のstate)
- Provider:不要
- 状態の共有:そのコンポーネント内のみ
- 使用場面:複雑なローカル状態の管理
💡 CounterWidget2.tsx:Redux ToolkitとuseReducerの組み合わせ
/src/containers/templates/CounterWidget2.tsx:24-26
const state, dispatch = useReducer(
counterSlice.reducer, // Redux ToolkitのReducerを流用!
initialCount,
);
Redux Toolkitで作ったsliceをuseReducerで使える!ActionもReducerもそのまま使い回し可能。
メリット:
- Redux Toolkitの簡潔な書き方を活用
- グローバル状態管理が不要な場合に軽量化
- 同じロジックをReduxとuseReducerで共有可能
code:比較.ts
⏺ 3つのディレクトリの比較
📁 02-redux(通常のRedux)
難易度: ⭐⭐⭐⭐ (高い)
- 型パズル(ValueOf<T>)が必要
- Action、ActionType、Reducerを別々に定義
- ボイラープレートコードが多い
- 不変更新を手動で管理
現在の使用状況: ❌ ほぼ使われていない
- レガシーコードのメンテナンスのみ
- 新規プロジェクトでは非推奨
- Redux公式も「Redux Toolkitを使うべき」と明言
コード量: 約50行(actions.ts + reducer.ts)
---
📁 04-toolkit(Redux Toolkit)
難易度: ⭐⭐ (低い)
- createSliceで全部まとめて定義
- 型は自動推論
- Immerで直接変更可能
- Redux DevTools自動設定
現在の使用状況: ✅ 現在の主流
- Redux公式推奨
- 2024年の新規プロジェクトの標準
- 最新バージョン2.9.0(2024年9月)
コード量: 約17行(counter.ts)
---
📁 05-use-reducer(useReducer)
難易度: ⭐⭐⭐ (中程度)
- Reduxの概念理解が必要
- グローバル状態管理はできない
- Props drilling問題が発生しやすい
現在の使用状況: ✅ 特定の場面で使用
- ローカル状態が複雑な場合に使用
- フォームやモーダルなど独立したコンポーネント
- 小規模アプリやRedux不要な場面
コード量: 約40行(CounterWidget.tsx内で完結)
---
🎯 使い分けの指針(2024年現在)
Redux Toolkit を選ぶべき場合
- 複数コンポーネント間で状態共有
- 大規模アプリケーション
- 時間旅行デバッグが必要
- チーム開発
useReducer を選ぶべき場合
- 単一コンポーネント内の複雑な状態
- 小規模アプリ
- Redux導入がオーバーキル
- 独立したウィジェット
通常のRedux を選ぶべき場合
- 新規では選ばない
- 既存コードの理解・保守のみ
📊 まとめ
| | 02-redux | 04-toolkit | 05-use-reducer |
|-------|----------|------------|----------------|
| 難易度 | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ |
| 現在も使用 | ❌ | ✅ | ✅ |
| 用途 | レガシー | グローバル状態 | ローカル状態 |
| コード量 | 多い | 少ない | 中程度 |
| 学習優先度 | 低 | 最高 | 高 |
推奨学習順序:
1. Redux Toolkit から始める(現在の標準)
2. useReducer を理解(Reactの基本機能)
3. 通常のReduxは必要に応じて(レガシーコード対応時)
code:本の意図.ts
📚 この本の構成の意図
02-04: Redux/Redux Toolkit を学ぶ
- グローバル状態管理の仕組みを理解
- Reduxの進化(通常→Toolkit)を体験
05: useReducer を紹介
「このカウンターアプリ程度なら、実はRedux要らないよね」という教訓
🎯 なぜこの順番なのか
1. Reduxでカウンター実装(02)
↓ 「動くけど、大げさ...」
2. Redux Toolkitで簡略化(04)
↓ 「楽になったけど、そもそも...」
3. useReducerで実装(05)
↓ 「これで十分じゃん!」
💡 重要な学び
このカウンターアプリの場合
- 1つのコンポーネントで完結
- 他のコンポーネントと共有不要
- useReducerで十分!
Reduxが必要になるのは
// 例:ECサイト
- ヘッダー:カート内商品数を表示
- 商品一覧:在庫数を表示
- カート画面:合計金額を計算
→ 複数箇所で同じ状態が必要 = Redux必要
✅ 実務での判断基準
状態管理の選択フロー:
1. 単純な状態?
→ useState
2. 複雑だけどローカル?
→ useReducer
3. 複数コンポーネントで共有?
→ Redux Toolkit
4. 2-3階層程度の共有?
→ Context API も検討
この本は「とりあえずRedux使う」という初学者の誤解を防ぐため、適材適所を教えている。
code:ts
🍎 useStateとuseReducerを簡単に理解する
📝 useState = メモ帳
一つずつ簡単なことを記録
// 例:TODOアプリの入力欄
const inputText, setInputText = useState('');
// 使い方はシンプル
setInputText('買い物に行く'); // 書き換えるだけ
📋 useReducer = チェックリスト
複雑な手順や条件がある時に使う
// 例:料理の手順
const cookingState, dispatch = useReducer(cookingReducer, {
step: '準備',
timer: 0,
temperature: 0
});
// 「次の工程」ボタンを押すと...
dispatch({ type: 'NEXT_STEP' });
// → 自動的に適切な温度・時間もセットされる
🎮 ゲームで例えると
useState = HPバーやスコア
const hp, setHp = useState(100);
const score, setScore = useState(0);
// ダメージを受けた
setHp(hp - 10); // 単純に減らすだけ
useReducer = ゲームの状態全体
const gameState, dispatch = useReducer(gameReducer, {
hp: 100,
mp: 50,
status: 'normal',
combo: 0
});
// 必殺技を使った
dispatch({ type: 'USE_SPECIAL_ATTACK' });
// → MPが減る、コンボがリセット、クールタイム開始...
// 複数のことが連動して起きる!
🏪 コンビニで例えると
useState = 個別の商品管理
const coffee, setCoffee = useState(10); // コーヒーの在庫
const bread, setBread = useState(5); // パンの在庫
// コーヒーを1つ売った
setCoffee(coffee - 1); // これだけ
useReducer = レジの処理
const registerState, dispatch = useReducer(registerReducer, {
items: [],
total: 0,
tax: 0,
change: 0,
points: 0
});
// 商品をスキャン
dispatch({ type: 'SCAN_ITEM', item: 'coffee' });
// → 商品追加、合計更新、税計算、ポイント計算が全部連動!
🚦 信号で例えると
useState = 単純な ON/OFF
const isRed, setIsRed = useState(true);
// 赤→青(単純な切り替え)
setIsRed(false);
useReducer = 信号機のサイクル
const signal, dispatch = useReducer(signalReducer, {
color: 'red',
timer: 30,
pedestrian: 'stop'
});
// 「次へ」ボタン
dispatch({ type: 'NEXT' });
// red→green→yellow→red...のサイクル
// 歩行者信号も連動して変わる
📱 実際のアプリでの判断基準
// 🟢 useState でOKな例
- モーダルの開閉: isOpen
- 入力中のテキスト: inputValue
- チェックボックス: isChecked
- タブの選択: activeTab
// 🔴 useReducer を検討すべき例
- 複数ステップのフォーム
- ショッピングカート(商品追加・削除・数量変更)
- 音楽プレイヤー(再生・停止・次へ・前へ・リピート)
- ゲームの状態管理
✅ 簡単な覚え方
useState:「これ」を「こう」変える(単純)
useReducer:「これをしたら」→「あれとこれとそれが変わる」(複雑)
1個のことならuseState、連鎖反応ならuseReducer!
code:ts
🎯 なぜ「迷ったらuseState」が良いのか
1. シンプルで理解しやすい
// ✅ 誰でも一瞬で理解できる
const isOpen, setIsOpen = useState(false);
// ❓ 理解に時間がかかる
const state, dispatch = useReducer(reducer, initialState);
// reducer の中身を見ないと何ができるか分からない
2. 移行が簡単
// useState → useReducer は簡単
// Before
const name, setName = useState('');
const email, setEmail = useState('');
// After(必要になったら移行)
const state, dispatch = useReducer(formReducer, {
name: '',
email: ''
});
3. デバッグが簡単
// ✅ useState:どこで変更されたか明確
setCount(10); // ここで10になった!
// ❌ useReducer:どのactionか探す必要
dispatch({ type: 'UPDATE' }); // reducer見ないと分からない
📊 段階的な複雑化の流れ
Step 1: useState で始める
const value, setValue = useState('');
Step 2: useState が増えてきた(3-4個)
const name, setName = useState('');
const email, setEmail = useState('');
const error, setError = useState('');
// → まだ大丈夫
Step 3: 関連性が出てきた
// email変更時にerrorもクリアしたい...
// name変更時に validation も走らせたい...
// → useReducer を検討
Step 4: 他のコンポーネントでも必要に
// → Redux Toolkit へ
💡 実務での判断基準
// 🟢 useState でスタート
const isLoading, setIsLoading = useState(false);
// こんな症状が出たら useReducer 検討:
- setStateの直後に別のsetState
- 同じタイミングで3つ以上のsetState
- 前の値に依存する複雑な更新
// それでも動くなら useState のままでOK!
🚦 チーム開発での鉄則
優先順位:
1. useState // 最優先(シンプル)
2. Redux Toolkit // グローバルが必要なら
3. useReducer // 本当に必要な時だけ
理由:
- 新メンバーが理解しやすい
- コードレビューが速い
- バグが起きにくい
✅ YAGNI原則
"You Aren't Gonna Need It"(必要になるまで作らない)
❌ 悪い例:
「将来複雑になるかも...」→ 最初から useReducer
✅ 良い例:
「今は単純」→ useState
「複雑になった」→ その時に useReducer へ移行
🎮 実例
// ゲームのスコア表示
// ✅ 最初はこれで十分
const score, setScore = useState(0);
const combo, setCombo = useState(0);
// 半年後...
// 「コンボでスコア倍率が変わる」
// 「特殊条件でボーナス」
// → この時点で初めて useReducer 検討
// それまでは useState で問題なく動いていた!
結論:過度な最適化は悪。迷ったら useState で始めて、本当に必要になってから useReducer/Redux を検討しましょう。