フロントエンド開発といえば。
react アプリの初期化( pnpm create vite@latest または npm init vite@latest <アプリ名> )

ReactのuseReducer を理解しやすくする

● Reducer とは?

Reducer の正体

Reducer は次の形をした関数です。

(これまでの結果, 新しく来た1つ) => 新しい結果

実際の式でいうとこの部分がreducerです

// sum には 10 が入ります。
const sum = [1, 2, 3, 4].reduce(  (acc, cur) => acc + cur  , 0 );
//                                ↑ Reducer                  ↑ 初期値

「reduce に渡すコールバック関数のことを Reducer と呼ぶ」とも言えます。

● reduce は Reducer を何度も呼び出している

reduce は、配列の要素を1つずつ Reducer に渡して呼び出します。Reducer の戻り値は、次の回の acc になります。

const reducer = (acc, cur) => acc + cur;
const sum = [1, 2, 3, 4].reduce(reducer, 0);
回 acc(これまでの結果) cur(新しく来た1つ) 戻り値(新しい結果)
1 0 1 1
2 1 2 3
3 3 3 6
4 6 4 10

役割を分けると次のようになります。

  • ループするのは reduce(呼び出す側)の仕事
  • Reducer は1回分の計算をするだけ

この「1回分の計算だけをする関数」という考え方が、そのまま React の useReducer につながります。

● useReducer の Reducer も同じ形

useReducer で使う Reducer も、形はまったく同じです。

(今の state, 起きた出来事) => 新しい state

「起きた出来事」のことを action と呼びます。

配列の reduce との違いは、入力がどこから来るかだけです。

配列の reduce useReducer
これまでの結果 acc state
新しく来た1つ 配列の要素 action
入力の来かた 配列から一気に ボタンのクリックなどで、時間をかけて1つずつ
呼び出す側 reduce React

つまり useReducer は、「時間をかけて1つずつ届く action を reduce している」と考えることができます。

● カウンターで実際に書いてみる

import { useReducer } from 'react';

// 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:
      return state;
  }
}

export default function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  //                                    ↑ Reducer  ↑ 初期値

  return (
    <div>
      <p>{state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+1</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
      <button onClick={() => dispatch({ type: 'reset' })}>リセット</button>
    </div>
  );
}

useReducer(reducer, { count: 0 }) は、配列の reduce(reducer, 0) とよく似た形をしています。Reducer と初期値を渡すところまで同じです。

useReducer が返すものは次の2つです。

  • state:今の状態
  • dispatch:action を React に届ける関数

ボタンを押すと dispatch({ type: 'increment' }) が呼ばれます。すると React が reducer(今の state, { type: 'increment' }) を呼び出し、戻り値が新しい state になります。

● 配列の reduce に置き換えてみる

ボタンを「+1、+1、-1」の順に押したとします。これは、次の配列の reduce と同じ結果になります。

const actions = [
  { type: 'increment' },
  { type: 'increment' },
  { type: 'decrement' },
];

const result = actions.reduce(reducer, { count: 0 });
// { count: 1 }

同じ Reducer を、配列の reduce にもそのまま渡せます。両者が同じ仕組みだということがよく分かります。

● Reducer を書くときの2つのルール

1. 純粋関数にする

同じ state と action を渡したら、必ず同じ結果を返すようにします。Reducer の中に、API 呼び出し・Math.random()・console.log 以外のログ送信などの副作用を入れてはいけません。副作用は、イベントハンドラや useEffect 側で行います。

2. state を直接書き換えず、新しいオブジェクトを返す

// NG:state を直接書き換えている
case 'increment':
  state.count += 1;
  return state;

// OK:新しいオブジェクトを返している
case 'increment':
  return { ...state, count: state.count + 1 };

React は、state が「新しいオブジェクトになったかどうか」で変更を判断します。直接書き換えて同じオブジェクトを返すと、画面が更新されません。

● useState と useReducer の使い分け

useState が向いている場面 useReducer が向いている場面
値が1つでシンプル state の項目が多い
更新の仕方が単純 更新パターンが多い
次の state が今の state に強く依存する
更新ロジックをコンポーネントの外に出してテストしたい

Reducer はただの関数なので、React を使わずに単体でテストできます。

expect(reducer({ count: 0 }, { type: 'increment' })).toEqual({ count: 1 });

これも useReducer の大きなメリットです。

No.2131
10/29 16:38

edit