第68章:useReducerのキモ!
Stateの型とActionの型をガッチリ定義しよう 🎯
この章では、useReducer を「なんとなく使える」から
**「型までちゃんと守れててドヤ顔できるレベル」**に引き上げます 💪✨
ポイントはこの2つだけです:
- State の型をハッキリ決める
- Action の型をハッキリ決める(=「どんな指示が飛んでくるか」を全部書き出す)
React v19 では useReducer の型推論もかなり賢くなっていて、
「reducer 関数+初期値」からいい感じに型を推論してくれます。
なので、ジェネリクスをゴリゴリ書くよりも、
State と Action の型をちゃんと定義して、reducer の引数に型を付けるのが今どきのおすすめスタイルです。(k8o)
1️⃣ ミニ例:カウンターを題材にするよ 🧮
この章では、次のようなシンプルなカウンターを例にします:
- 「+」ボタンでカウントアップ
- 「-」ボタンでカウントダウン
- 「リセット」ボタンで
0に戻す
UIは次の章でじっくり作り直すとして、 ここでは型と reducer の書き方に集中します 👀
2️⃣ State の型を決める 🧾
まずは「このカウンターアプリの状態ってなに?」を言葉で整理します。
「いまのカウントの数字(
count)だけあれば十分だよね?」
なので State はこんな形でOKです:
// CounterState: アプリが覚えておきたい「状況」
type CounterState = {
count: number;
};
これで、
state.countは必ずnumbercount以外の謎プロパティは登場しない
という安心できる土台ができます 😊
3️⃣ Action の型を決める ✉️
次に、「どんな指示で state を変えるのか?」をぜんぶ列挙します。
今回のカウンターだと:
- カウントアップ →
"increment" - カウントダウン →
"decrement" - リセット(0 に戻す) →
"reset"
みたいな「指示書(Action)」をイメージできます 📮
これを TypeScript のユニオン型で表現するとこうなります:
// CounterAction: 「どんな指示が飛んでくるか」の一覧
type CounterAction =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'reset'; payload: number };
ポイント ✨
-
typeプロパティの文字列で「どの指示か」を見分ける -
必要に応じて追加情報を
payloadなどの名前でくっつける- ここでは「どの値にリセットするか」を
payloadで受け取る例にしてます
- ここでは「どの値にリセットするか」を
たとえば、こんな Action が有効です:
{ type: 'increment' }
{ type: 'decrement' }
{ type: 'reset', payload: 0 }
{ type: 'reset', payload: 10 }
逆に、こういうのは 全部エラーにできる のが型定義のうれしいところ 🥰
{ type: 'incremnt' }← スペルミス ❌{ type: 'reset' }←payloadが足りない ❌{ type: 'reset', payload: '0' }←payloadが文字列 ❌
4️⃣ reducer 関数に State と Action の型をつける 🧠
useReducer の中心人物が reducer 関数 でしたね。
「古い state」と「action(指示)」を渡したら、 「新しい state」が返ってくる関数
これを TypeScript で書くと、こうなります 👇
function counterReducer(
state: CounterState,
action: CounterAction
): CounterState {
switch (action.type) {
case 'increment': {
return { count: state.count + 1 };
}
case 'decrement': {
return { count: state.count - 1 };
}
case 'reset': {
return { count: action.payload };
}
default: {
// ここには絶対来ないはず!
const _exhaustiveCheck: never = action;
return state;
}
}
}
ここが 第68章のいちばん大事なポイント です ✨
stateの型 →CounterStateactionの型 →CounterAction- 戻り値の型 → ふたたび
CounterState
こうしておくことで、
state.countを変な型にしようとするとエラーaction.typeに存在しない文字列を書いたらエラーresetなのにpayloadを忘れたらエラー
…という感じで、「変なバグ」がコンパイル時点で止まってくれます 💘
🧪 おまけ:default で Action の漏れを検出するテク
default の中で
const _exhaustiveCheck: never = action;
と書いているのは、
「もし
CounterActionに新しいパターンを追加したのに、 switch にcaseを書き忘れたらエラーしてほしい!」
という保険です。
たとえば、新しく { type: 'multiply'; payload: number } を追加したのに、
switch に case 'multiply': を書き忘れると、
TypeScript が「action は never じゃないよ!」と怒って教えてくれます 🥹💡
5️⃣ React v19 での useReducer の型推論 🧬
React v19 / @types/react@19 では、useReducer の型推論が強化されています。
- React 18 までは「reducer 関数だけ」から state の型を推論
- React 19 からは reducer 関数+初期値 から state を推論するスタイルに変更されました (k8o)
その結果、たとえばこんな書き方でも:
const [state, dispatch] = useReducer(
(prevCount: number) => prevCount + 1,
0
);
ちゃんと state が number と推論されるようになっています 🎉(k8o)
さらに、React 19 のアップグレードガイドでは、
useReducer<React.Reducer<State, Action>> みたいなジェネリクス指定は基本もう不要で、
reducer 関数に型を付けて、useReducer(reducer, initialState) と書くのが推奨と説明されています。(angularminds.com)
👉 この章でやった
type CounterState = { ... }type CounterAction = ...function counterReducer(state: CounterState, action: CounterAction): CounterState { ... }
というスタイルは、まさにReact 19 時代の正攻法です ✨
6️⃣ いよいよ useReducer を呼び出す ✨
では、実際にコンポーネントの中で useReducer を使ってみましょう。
Counter.tsx みたいなファイルをイメージしてOKです 🙆♀️
import { useReducer } from 'react';
type CounterState = {
count: number;
};
type CounterAction =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'reset'; payload: number };
function counterReducer(
state: CounterState,
action: CounterAction
): CounterState {
switch (action.type) {
case 'increment': {
return { count: state.count + 1 };
}
case 'decrement': {
return { count: state.count - 1 };
}
case 'reset': {
return { count: action.payload };
}
default: {
const _exhaustiveCheck: never = action;
return state;
}
}
}
const initialState: CounterState = { count: 0 };
export function Counter() {
const [state, dispatch] = useReducer(counterReducer, initialState);
const handleIncrement = () => {
dispatch({ type: 'increment' });
};
const handleDecrement = () => {
dispatch({ type: 'decrement' });
};
const handleReset = () => {
dispatch({ type: 'reset', payload: 0 });
};
return (
<div>
<p>Count: {state.count}</p>
<button onClick={handleIncrement}>+1</button>
<button onClick={handleDecrement}>−1</button>
<button onClick={handleReset}>リセット</button>
</div>
);
}
🧡 ここでの「型のいいところ」まとめ
-
dispatchに渡せるオブジェクトは 必ずCounterActionのどれか -
ボタンごとの
dispatchコードが自己紹介みたいにわかりやすいdispatch({ type: 'increment' })→ 「インクリメントしなさい」
-
reducer の
switchが Action 一覧の「対応表」 になっていて読みやすい
7️⃣ 図でイメージしてみる 🎨(Mermaid)
useReducer の流れをカンタンな図で整理しておきます 🧠
この図のイメージ:
- ボタンがクリックされる(ユーザー操作)
- コンポーネント内で
dispatch({ type: 'increment' })を呼ぶ counterReducerが呼ばれて、新しいstateを計算- React が新しい
stateで画面を描き直す
State と Action の型をガッチリ決めることで、 この流れのどこかで変なオブジェクトが紛れ込むのを、 TypeScript がしっかりガードしてくれます 🛡️✨
8️⃣ ミニ練習問題 💪(ノートに書いてみて!)
時間があれば、VSCode かノートに次の型だけ考えてみてください ✍️
「ToDoリストの reducer を作るとしたら、 State と Action の型はどう定義する?」
ヒント:
-
State の候補 👉
Todoの配列 (Todo[]) -
Todoの型 👉{ id: number; title: string; done: boolean }みたいなイメージ -
Action の候補 👉
- 追加:
{ type: 'add'; payload: { title: string } } - 完了フラグ切り替え:
{ type: 'toggle'; payload: { id: number } } - 削除:
{ type: 'remove'; payload: { id: number } }
- 追加:
「どんな指示が飛んできそう?」を日本語で書き出してから Action のユニオン型に落とし込むのがコツだよ 🧡
9️⃣ この章のゴールおさらい 🎉
- ✅ State の「形」を
typeでハッキリさせられるようになった - ✅ Action を「指示書」としてユニオン型で定義できるようになった
- ✅ reducer 関数の引数に
StateとActionの型を付けて、 「型安全で読みやすい」useReducerパターンを理解できた - ✅ React v19 では
useReducerの型推論が強くなっていて、 reducer と初期値から自動で型をいい感じに出してくれる、ってことも知れた (k8o)
次の 第69章 では、
「じゃあ
useStateとuseReducer、 実際どっちをいつ使うのがいいの?」
という実践的な選び方を一緒に見ていきます 🌈
その次の 第70章 で、この章のカウンターをフルで useReducer 版に作り直して、
がっつり手を動かしていきましょ〜 ✨🧃