useReducer 完全入門【React】複雑な状態管理をスッキリ整理する
ReactのuseReducerとは何か、reducer関数とdispatchの使い方を初心者向けに解説。useStateと使い分けるべき場面や、複数の状態をまとめて管理するパターンを紹介します。
useState を使っていると、状態が増えるにつれてコンポーネントが複雑になっていきませんか?「追加」「削除」「編集」「完了」など、複数のアクションがある場合、更新ロジックがどんどん増えて管理が難しくなります。
そんなときに役立つのが useReducer です。状態の更新ロジックを「reducer(リデューサー)関数」としてまとめることで、コードが整理しやすくなります。
この記事でわかること:
useReducerの仕組みと基本的な使い方- reducer 関数と dispatch の書き方
useStateとの使い分けの基準- ToDoアプリを例にした実践的な実装
useReducer とは?
useReducer は、状態の更新ロジックをコンポーネントの外に切り出すフックです。
const [state, dispatch] = useReducer(reducer, initialArg, init?);| パラメータ | 説明 |
|---|---|
reducer | 状態更新のロジックを定義した関数 |
initialArg | 初期 state の値 |
init(省略可能) | 初期化関数(指定するとinit(initialArg)の結果が初期値になる) |
| 戻り値 | 説明 |
|---|---|
state | 現在の state |
dispatch | アクションを発行して state を更新する関数 |
reducer 関数の書き方
reducer 関数は (現在のstate, action) => 次のstate という形の純粋関数です。
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
case "reset":
return { count: 0 };
default:
throw new Error(`Unknown action: ${action.type}`);
}
}重要:state を直接書き換えてはいけません。必ず新しいオブジェクトを返します。
基本的な使い方
import { useReducer } from "react";
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
<p>カウント: {state.count}</p>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
</>
);
}dispatch にはアクションオブジェクトを渡します。慣例として type プロパティで何をするかを指定し、追加データがあれば一緒に渡します。
実践例:ToDoアプリ
import { useReducer } from "react";
// reducer 関数(コンポーネントの外に定義する)
function todoReducer(todos, action) {
switch (action.type) {
case "added":
return [
...todos,
{ id: Date.now(), text: action.text, done: false },
];
case "toggled":
return todos.map((todo) =>
todo.id === action.id ? { ...todo, done: !todo.done } : todo
);
case "deleted":
return todos.filter((todo) => todo.id !== action.id);
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
function TodoApp() {
const [todos, dispatch] = useReducer(todoReducer, []);
const [text, setText] = useState("");
const handleAdd = () => {
if (!text.trim()) return;
dispatch({ type: "added", text }); // アクションを発行
setText("");
};
return (
<div>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="タスクを入力"
/>
<button onClick={handleAdd}>追加</button>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => dispatch({ type: "toggled", id: todo.id })}
/>
<span style={{ textDecoration: todo.done ? "line-through" : "" }}>
{todo.text}
</span>
<button onClick={() => dispatch({ type: "deleted", id: todo.id })}>
削除
</button>
</li>
))}
</ul>
</div>
);
}useState vs useReducer の使い分け
| 状況 | 使うフック |
|---|---|
| 単一の値(数値・文字列・真偽値) | useState |
| 独立した複数の値 | useState(複数回) |
| 関連する複数の値をまとめて更新 | useReducer |
| 複数のアクションによる複雑な更新ロジック | useReducer |
| 次の state が前の state に依存する | useReducer |
目安として:
- 3つ以下の独立した state →
useStateがシンプル - 同じイベントで複数の state が変わる →
useReducerを検討 - 更新ロジックが複雑でテストしたい →
useReducer(reducer 関数は純粋関数なので単体テストしやすい)
初期化関数を使う
計算コストが高い初期値の場合、第3引数に初期化関数を渡せます。
function createInitialState(initialCount) {
return { count: initialCount, history: [] };
}
// ✅ 初回レンダリングのときだけ createInitialState が実行される
const [state, dispatch] = useReducer(reducer, 0, createInitialState);
// → { count: 0, history: [] } が初期値になるまとめ
useReducerは状態の更新ロジックを reducer 関数にまとめるフックdispatch({ type: "actionType", ...payload })でアクションを発行する- reducer 関数は純粋関数で、常に新しい state オブジェクトを返す
- 関連する複数の値・複雑な更新ロジックがある場合に
useStateより適している - reducer 関数をコンポーネントの外に定義することで、ロジックが整理しやすくなる
PR
useReducer 公式ドキュメントで詳細を確認しよう
useReducerのインタラクティブなサンプルや、ImmerとuseReducerを組み合わせたパターンは公式ドキュメントで確認できます。
useReducer 公式ドキュメントを見る →