Reducer の正体
Reducer は次の形をした関数です。
(これまでの結果, 新しく来た1つ) => 新しい結果
実際の式でいうとこの部分がreducerです
// sum には 10 が入ります。
const sum = [1, 2, 3, 4].reduce( (acc, cur) => acc + cur , 0 );
// ↑ 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(呼び出す側)の仕事この「1回分の計算だけをする関数」という考え方が、そのまま React の useReducer につながります。
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 になります。
ボタンを「+1、+1、-1」の順に押したとします。これは、次の配列の reduce と同じ結果になります。
const actions = [
{ type: 'increment' },
{ type: 'increment' },
{ type: 'decrement' },
];
const result = actions.reduce(reducer, { count: 0 });
// { count: 1 }
同じ Reducer を、配列の reduce にもそのまま渡せます。両者が同じ仕組みだということがよく分かります。
同じ state と action を渡したら、必ず同じ結果を返すようにします。Reducer の中に、API 呼び出し・Math.random()・console.log 以外のログ送信などの副作用を入れてはいけません。副作用は、イベントハンドラや useEffect 側で行います。
// NG:state を直接書き換えている
case 'increment':
state.count += 1;
return state;
// OK:新しいオブジェクトを返している
case 'increment':
return { ...state, count: state.count + 1 };
React は、state が「新しいオブジェクトになったかどうか」で変更を判断します。直接書き換えて同じオブジェクトを返すと、画面が更新されません。
| useState が向いている場面 | useReducer が向いている場面 |
|---|---|
| 値が1つでシンプル | state の項目が多い |
| 更新の仕方が単純 | 更新パターンが多い |
| 次の state が今の state に強く依存する | |
| 更新ロジックをコンポーネントの外に出してテストしたい |
Reducer はただの関数なので、React を使わずに単体でテストできます。
expect(reducer({ count: 0 }, { type: 'increment' })).toEqual({ count: 1 });
これも useReducer の大きなメリットです。