useReducer は、useState のようにstateを保持しつつ、それを変えるすべての方法をreducerという1つの関数にまとめるReactのフックです。コンポーネントは { type: 'increment' } のようなアクションオブジェクトで dispatch を呼び、Reactは現在のstateとそのアクションをreducerに渡し、reducerが次のstateを返します。
ボタンはカウントがどう変わるかを語らず、何が起きたかだけを伝えます。計算はすべて reducer の中にあります。{ count: state.count * 2 } を返す case 'double' と、それをdispatchするボタンを追加して、新しい種類の更新がどう収まるかを見てみてください。
構文
const [state, dispatch] = useReducer(reducer, initialArg, init); // init is optional
function reducer(state, action) {
// return the next state
}
reducerは(state, action) => nextStateという関数です。コンポーネントの外で定義してください。コンポーネントのスコープにあるものは何も必要ありません。initialArgは初期stateで、最初のレンダリングでだけ使われます。initは省略可能です。渡すと、初期stateは代わりにinit(initialArg)になります。stateはこのレンダリングでの現在のstateです。dispatch(action)はアクションをreducerに送り、その結果でのレンダリングを予約します。これは安定しています。すべてのレンダリングで同じ関数なので、下へ渡したり依存配列に入れたりしても安全です。
アクションはどんな値でも構いませんが、慣習として、何が起きたかを表す type 文字列と、reducerが必要とするデータを持つオブジェクトにします:{ type: 'added', text: 'Buy milk' }。
reducerを書く
ほとんどのreducerは action.type に対する switch で、イベントの種類ごとに case が1つあります。各caseは真新しいstateを返し、古いstateは決して変えません。default のcaseは例外を投げるので、dispatch({ type: 'incremnet' }) のようなタイプミスは何もしないのではなく、はっきりと失敗します。
アクションには、思い描いているstateの変更(setTodos)ではなく、ユーザーがしたこと(added、toggled、deleted)にちなんだ名前を付けます。そうするとコンポーネントはイベントの一覧のように読め、各イベントの意味を決める場所はreducerだけになります。
reducerは純粋でなければならない
reducerは純粋関数です。同じstateとアクションが与えられれば同じ結果を返し、それ以外のことは何もしません。つまり、中で直接変更、リクエスト、タイマー、Math.random() や Date.now() を使ってはいけません。Reactはこれを前提にしています。開発中のStrictModeでは、純粋でないreducerに気づけるよう、Reactは各アクションについてreducerを2回呼び、1つの結果を採用します。ここのプレビューは本番ビルドのように動くので、このページの例は1回だけ呼びます。
一番よく破られるのは直接変更のルールです。state.todos にpushして state を返すと同じオブジェクトが返るので、Reactは変化がないと見なしてレンダリングを省きます。配列とオブジェクトの更新で説明したのと同じ落とし穴です。スプレッド、map、filter で新しい配列やオブジェクトを作ってください。
// Wrong: mutates and returns the same object
case 'added':
state.todos.push(action.todo);
return state;
// Right: returns a new object with a new array
case 'added':
return { ...state, todos: [...state.todos, action.todo] };
サーバーへの保存のようにイベントに属する副作用は、dispatch の隣のイベントハンドラに書きます。localStorage との同期のようにstateに属する副作用は、エフェクトに書きます。
useReducerでTodoリストを作る
ここではreducerが本格的に働きます。Todoの追加、切り替え、削除です。reducerの先頭の console.log はすべてのアクションを出力するので、デバッグ中にstateがどう変わるかを見るのに便利です。何も変えないので、reducerの中で一般的に許容される唯一の副作用ですが、リリース前には取り除いてください。
Todoを追加し、チェックを入れ、1つ削除してから、コンソールを読んでください。どの変更も、アクションとそのデータを示す1行になっています。入力欄のテキストは useState のままです。その欄だけのもので、ほかのイベントは触れないからです。1つのコンポーネントで2つのフックを組み合わせるのはごく普通のことです。
新しいTodoのidはクリックハンドラで作られてアクションに乗って運ばれるので、reducerはそれをコピーするだけです。reducerの中で id: nextId++ と書くと純粋でなくなります。開発中のStrictModeでは、2回目の呼び出しでidが1つ飛んでしまいます。
dispatchはすぐにはstateを変えない
dispatch は useState のセッターと同じように動きます。レンダリングを予約し、現在のハンドラの中の state 変数は古い値を保ちます。新しいstateをすぐに使いたいなら、reducerを呼んで自分で計算してください。
何回かクリックしてください。1行目のログは常に1つ遅れていて、2行目はレンダリング後にボタンに表示される数値と一致します。reducerは普通の関数なので、自分で呼んでも安全です。これも純粋に保つことの利点です。
useStateとuseReducerの違い
どちらのフックもstateを保存し、一方で書けるものはもう一方でも書けます。違いは、更新ロジックをどこに置くかです。
| useState | useReducer | |
|---|---|---|
| 更新ロジック | 各イベントハンドラの中 | 1つのreducer関数の中 |
| 向いている用途 | 少数の独立した値 | 多くのイベントで一緒に変わる複数のフィールド |
| コード量 | 単純なstateでは少ない | 最初は多いが、イベントが増えると少なくなる |
| テスト | コンポーネントを通じてテスト | reducerを普通の関数としてテスト |
| デバッグ | どのハンドラがセットしたかを探す | 1か所で各アクションをログに出す |
同じstateが多くのハンドラで更新されているのに気づいたとき、1つのイベントが一貫性を保つべき複数のstateを変えなければならないとき、またはハンドラの中身がほとんど次のstateについてのロジックであるときは、useReducer を使いましょう。トグルやテキスト欄なら、useState のほうが短くて明快です。後から切り替えるのも難しくありません。セッターをdispatchに置き換え、ロジックをcaseに移すだけです。
遅延初期化
useReducer(reducer, createInitialState('Ada')) のように初期stateを関数呼び出しで書くと、Reactがその結果を使うのは最初の1回だけなのに、その呼び出しはレンダリングのたびに実行されます。初期stateの構築が重い場合や、propから導くべき場合は、第3引数として init 関数を渡します。Reactは最初のレンダリングで1回だけ init(initialArg) を呼びます。
入力欄に入力してください。コンポーネントはキーを押すたびにレンダリングされますが、createInitialState がログを出すのは1回だけです。次に呼び出しを useReducer(reducer, createInitialState('Ada')) に変えてもう一度入力してください。関数はレンダリングのたびに実行され、Reactは最初の1回以降、毎回その結果を捨てます。
渡すのは関数を呼んだ結果ではなく、関数そのものです。useReducer(reducer, 'Ada', createInitialState) は遅延されますが、useReducer(reducer, createInitialState('Ada')) は遅延されません。
useReducerとコンテキスト
深いツリーがstateを必要とするとき、reducerはコンテキストと相性がよいです。stateと dispatch を一番上でコンテキストに入れれば、その下のどのコンポーネントも、すべての層にpropsを通さずにstateを読んだりアクションを送ったりできます。dispatch は決して変わらないので、アクションを送るだけのコンポーネントは別のdispatch用コンテキストを読めば、stateが変わっても再レンダリングされずに済みます。
import { createContext, useContext, useReducer } from 'react';
const TodosContext = createContext(null);
const TodosDispatchContext = createContext(null);
export function TodosProvider({ children }) {
const [todos, dispatch] = useReducer(todosReducer, []);
return (
<TodosContext value={todos}>
<TodosDispatchContext value={dispatch}>{children}</TodosDispatchContext>
</TodosContext>
);
}
function AddTodo() {
const dispatch = useContext(TodosDispatchContext);
return <button onClick={() => dispatch({ type: 'added', text: 'New' })}>Add</button>;
}
React 19では、コンテキストオブジェクトがそれ自体でプロバイダーとして動きます(<TodosContext value={...}>)。<TodosContext.Provider> も引き続き使えます。コンテキストがどうコンポーネントに届き、いつ再レンダリングするかは、useContextのページで扱っています。
よくある質問
ReactのuseReducerとは何ですか?
更新をアクションとして記述するstateのためのフックです。const [state, dispatch] = useReducer(reducer, initialState) と呼び、イベントハンドラから dispatch({ type: 'added' }) を呼びます。Reactは現在のstateとアクションを reducer に渡し、その戻り値が次のstateになります。
useStateではなくuseReducerを使うべきなのはどんなときですか?
複数のイベントが同じstateを関連した形で更新するとき、次のstateが複数のフィールドに同時に依存するとき、または更新ロジックが長く、コンポーネントの外のテストしやすい1つの関数にまとめたいときです。独立した値が1つか2つなら、useStateのほうが簡単です。
reducerが純粋でなければならないのはなぜですか?
Reactは同じアクションに対してreducerを複数回呼ぶことがあり(開発中のStrictModeは意図的にそうします)、毎回同じ結果を期待するからです。そのためreducerは、stateの直接変更、データ取得、タイマーの設定、ランダムな値の読み取りをしてはいけません。それらはイベントハンドラかエフェクトで行ってください。
dispatchはstateをすぐに更新しますか?
いいえ。useStateのセッターと同じく、dispatch はレンダリングを予約します。現在のイベントハンドラの中では state は古い値のままで、新しいstateは次のレンダリングで現れます。
useReducerの第3引数は何ですか?
省略可能な init 関数です。これを渡すと、Reactは初期stateを init(initialArg) として、最初のレンダリングでだけ計算します。初期stateの構築が重いときや、propに依存するときに便利です。