Menu

React useReducerの使い方:構文、例、useStateとの違い

useReducerはstateを保存し、すべての更新を1つのreducer関数にまとめます。アクションをdispatchすると、reducerが次のstateを返します。構文、純粋なreducerの書き方、Todoリストの例、遅延初期化、そしてuseStateより選ぶべき場面を学びます。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

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を保存し、一方で書けるものはもう一方でも書けます。違いは、更新ロジックをどこに置くかです。

useStateuseReducer
更新ロジック各イベントハンドラの中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に依存するときに便利です。

Coddyのプログラミング言語のイラスト

Coddyでコードを学ぼう

始める