https://qiita.com/uhyo/items/cea1bd157453a85feebf

React Hooksの正式リリース(2019年2月)からそろそろ一年が経とうとしています。Hooksの登場によってReactのコンポーネントは関数コンポーネントが一気に主流になり、クラスコンポーネントが新規に作られる機会は激減しました。

また、React 17.x系ではConcurrent Modeの導入とともにさらに2種類の新フックが追加される見込みであり、いよいよ関数コンポーネントの能力がクラスコンポーネントを真に上回る時代が来ることになります。

この記事では、フックの一種であるuseReducerに焦点を当てて、どのようなときにuseReducerが適しているのかを説明します。究極的には、useReducerによって達成できるパフォーマンス改善があり、ときにはそれがコンポーネント設計にまで影響を与えることを指摘します。

useStateの影に隠れたり、なぜかReduxと比較されたりといまいちぱっとしないuseReducerですが、この記事でその真の魅力を知っていただければ幸いです。

背景: useReducerとは

まずは、初心者の方向けにuseReducerの動作を説明します。すでに知っているという方は次の節まで飛ばしても構いません。

useReducerはフックの一種であり、関数コンポーネントのステートを宣言する能力を持ちます。ステートの宣言はuseStateuseReducerの2種類の方法がありますが、useReducerは複雑なロジックが絡んだステートを宣言するのに適しています。

useReducerは以下のように使います。こちらが用意するのはreducerinitialStateの2つです。reducerは「現在のステート」と「アクション」を受け取って「新しいステート」を返す関数であり、initialStateはステートの初期値です。

useReducerの返り値は2つで、currentStateはステートの現在の値、dispatchはアクションを発火する関数です。dispatchにアクションを渡すと、内部でreducerが呼び出されて新しいステートが計算され、コンポーネントが再レンダリングされて新しいステートが反映されます。

一応簡単な例を示しておきます。まずはreducerの例です。分かりやすさのためにTypeScriptを用いています。

type State = {
  count: number
};

type Action = {
  type: "increment" | "decrement";
};

const reducer = (state: State, action: Action): State => {
  if (action.type === "increment") {
    return {
      count: state.count + 1
    };
  } else {
    return {
      count: state.count - 1
    }
  }
};

ここではアクションは{ type: "increment" }または{ type: "decrement" }です。見て分かる通り、これはそれぞれ「カウンタを1増やす」操作と「カウンタを1減らす」操作に相当します。このreducerによって管理されるState{ count: number }です。つまり、カウンタの数値をひとつ持っているだけのオブジェクトです。この場合type State = numberでも別に構いませんが、今後の拡張性を考えてこの定義にしています。

これはconst [state, dispatch] = useReducer(reducer, { count: 0 })のように使用します。このdispatchを用いて、dispatch({ type: "increment" })とすればステートが変化してカウンタの値が1増えるでしょう。

これがuseReducerの使い方です。useReducerは、ステートの種類が増えたりロジックが増えたりしてもその操作の窓口がdispatchという一点に集約されている点がポイントです。子コンポーネントが何かしらのロジックを発火したいときはdispatchをpropsで渡すだけでいいし、コンポーネントツリーが大きい場合はコンテキストを用いて子に伝えるのも有効でしょう。

useReducerがパフォーマンス改善につながる例