useReducer は、state の更新規則を reducer 関数に集める Hook です。フォーム、編集画面、複数の操作で同じ state を変える UI では、イベントハンドラを読みやすくできます。
action で「何をしたか」を表す
reducer は現在の state と action を受け取り、次の state を返す純粋な関数です。コンポーネントは dispatch で action を送るだけにします。
import { useReducer } from 'react';
type Action = { type: 'increment' } | { type: 'decrement' };
function reducer(count: number, action: Action) {
switch (action.type) {
case 'increment': return count + 1;
case 'decrement': return count - 1;
}
}
function Counter() {
const [count, dispatch] = useReducer(reducer, 0);
return <button onClick={() => dispatch({ type: 'increment' })}>{count}</button>;
}
実務では state をオブジェクトにし、状態遷移を action ごとに明示することが多くあります。
type FormState = { name: string; submitted: boolean };
type FormAction =
| { type: 'changedName'; name: string }
| { type: 'submitted' }
| { type: 'reset' };
function formReducer(state: FormState, action: FormAction): FormState {
switch (action.type) {
case 'changedName': return { ...state, name: action.name };
case 'submitted': return { ...state, submitted: true };
case 'reset': return { name: '', submitted: false };
}
}
更新の入口が action に集まるので、どの操作がどのフィールドを変え得るかを reducer だけで確認できます。
action は「増やす」という意図、reducer は「どう state を変えるか」という規則を持ちます。この分離により、複数のイベントから同じ更新規則を再利用できます。
たとえば「保存開始」「保存成功」「保存失敗」がある編集画面なら、各ハンドラで複数の setter を呼ぶ代わりに action を dispatch します。画面を読む人は action の一覧から可能な操作を、reducer から操作ごとの state 遷移を確認できます。TypeScript では action を判別可能な union にすると、新しい操作を追加したときに switch の漏れを見つけやすくなります。
useState との選び方
単純な値の更新なら useState の方が短く分かりやすいです。状態が相互に関係する、操作の種類が多い、更新規則をテストしたい場合に useReducer を選びます。reducer はコンポーネント外へ出せるため、UI を描画せずに入力と出力を検証できます。
落とし穴
reducer 内で API 呼び出し、日時の生成、元のオブジェクトの変更をしてはいけません。毎回同じ入力なら同じ出力を返すようにします。初期 state の構築が重い場合は第3引数に initializer を渡せます。
dispatch の直後に state 変数の値が変わるわけではありません。現在のレンダーの state はスナップショットであり、次のレンダーで reducer の戻り値が反映されます。
reducer が未知の action を受け取ったら、開発中はエラーにして漏れを見つける方法もあります。単に現在の state を返すと、誤字のある action が静かに無視されます。どちらを選ぶかは action の発行元をどこまで制御できるかで決めます。
Context と組み合わせるとき
複数の離れたコンポーネントから同じ更新規則を使う場合、state と dispatch を Context で提供できます。ただし、更新のたびに state を読む全コンポーネントが更新対象になります。読み取り専用の Context と dispatch 専用の Context を分けるかは、再レンダーが実際に問題になってから検討します。まず reducer の責務を小さくし、どの画面の state かが分かる位置に Provider を置きます。
まとめ
useReducer は複雑になった state 更新を整理するための選択肢です。state が小さい間は useState を使い、更新規則が UI より目立ち始めたら reducer へ抽出します。