Reactのフックの一つにuseReducerがある。いったい何を減らしているの。地味に気になってムズムズするのでちょっと調べたり考えたりしてみたことをまとめる。

関数プログラミングにおけるリデューサー関数

関数型プログラミング言語には、fold(畳み込み)と呼ばれる関数群が用意されている(関数プログラミングをよく知らないのでWikipediaの受け売りだ)。これらの関数は、複数の値を持つデータ構造を1つの値にまとめる特徴を持つ。このため、reduce, accumulate, aggregate, compressなどとも呼ばれる。これらは、同じ引数を与えると必ず同じ演算結果を返す純粋関数である。

JavaScriptで言えば、array.reduce()が該当する。このケースは名称と処理が直感的に一致しているので分かりやすい。

js
console.log([1, 2, 3, 4, 5].reduce((accum, curVal) => accum + curVal));
// 出力:15

[1,2,3,4,5]という5つの値が15という1つの値にリデュースされた(減らされた)。と言っても見かけ上の話で、実際には元の配列の値は減っていない。reduce()メソッドのコールバックに渡される関数は、常に新たな別の値を返す。

例えばオブジェクトをあれこれする場合は、スプレッド構文などを使って、コピーしたオブジェクトを使うことが求められる。

話をややこしくしてしまった。とりあえず、関数プログラミングにおけるリデュース関数は、コレクション(リストや配列など)の各要素に対して純粋関数を繰り返し適用し、1つの値を返す。

そして、このreduce()メソッドに渡されている、値の合体を担う関数こそreducerである。reduce()メソッドは1つの値を返す。ただ、その処理の中で繰り返し適用されるリデューサー関数は、正確には「次のリデューサー関数に渡される値」を返す。

この点に注目すれば、useReducerがなぜReducerと名付けられたのか類推できる。

状態管理におけるリデュース関数

ReactフックのuseReducer()は、関数プログラミングのリデュース関数を状態管理に応用したものである。

以下は、Reactのドキュメントに記載されているサンプルコードだ。

ts
import { useReducer } from "react";

interface State {
  count: number;
}

type CounterAction =
  | { type: "reset" }
  | { type: "setCount"; value: State["count"] };

const initialState: State = { count: 0 };

function stateReducer(state: State, action: CounterAction): State {
  switch (action.type) {
    case "reset":
      return initialState;
    case "setCount":
      return { ...state, count: action.value };
    default:
      throw new Error("Unknown action");
  }
}

export default function App() {
  const [state, dispatch] = useReducer(stateReducer, initialState);

  const addFive = () => dispatch({ type: "setCount", value: state.count + 5 });
  const reset = () => dispatch({ type: "reset" });

  return (
    <div>
      <h1>Welcome to my counter</h1>

      <p>Count: {state.count}</p>
      <button onClick={addFive}>Add 5</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

配列ではないし、集約される最終的な出力も見えない。ただ、状態(CounterAction)をコレクションと捉えると、useReducerは関数プログラミングで言うリデューサーそのものの働きをしている。

つまり、同じデータ構造を持つ一連の値に繰り返し適用され、その返り値は次の関数に渡される。入力に対する出力は不変で、その値を変更することもない。

useStateMachineuseStateForwarderなどとするより、useReducerとした方が、関数プログラミングに馴染みのある開発者にとって分かりよかったのではないかと邪推する。

参考資料