useReducerについて改めて勉強してみた

2021-11-27

React 標準の hooks はぼちぼち使ってきた自分ですが、useReducer だけはどうもしっくり来ず使っていませんでした。

ということで、己の凝り固まった考え方を溶かすべく、useReducer について勉強していこうと思います。


そもそも useReducer とは

useReducer

useState の代替品です

とのことなので、useState で書けるものは useReducer に書き換えることが可能ってことみたいです。

やはりイメージは Redux ですよね。

useReducer のメリット

useReducer の使用が useState より好ましいとされるケースは以下の通りらしいです。

  1. 複数の値にまたがる複雑な state ロジックがある場合
  2. 前の state に基づいて次の state を決める必要がある場合

1 つずつ自分なりに解釈をば。

複数の値にまたがる複雑な state ロジック

たとえば自分はよく以下のようなイメージのコードを書いていました。(動作未確認ですが…)

function Hoge(): JSX.Element {
  const [count, setCount] = useState(0);
  const [isEven, setIsEven] = useState(!(count % 2));
  const handleClick = useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, []);

  useEffect(() => {
    setIsEven(!(count % 2))
  }, [count]);

  return (
    <>
      <button onClick={handleClick}>
        INCREMENT COUNT
      </button>
      <div>{`Count is ${isEven ? "even" : "odd"}.`}</div>
    </div>
  );
}

この場合、state を 2 つ切ることになるのでちょっと記述が冗長的ですよね。

で、これを useReducer で書き直すと。

const initialState = {
  count: 0,
  isEven: true
}

function reducer(state, { type }) {
  switch (type) {
    case "increment": {
      const { count } = state;
      const nextCount = count + 1;

      return {
        count: nextCount,
        isEven: !(isEven % nextCount)
      };
    }
    default: {
      return state;
    }
  }
}

function Hoge(): JSX.Element {
  const [{ isEven }, dispatch] = useReducer(reducer, initialState);
  const handleClick = useCallback(() => {
    dispatch({ type: "increment" })
  }, []);

  return (
    <>
      <button onClick={handleClick}>
        ADD COUNT
      </button>
      <div>{`Count is ${isEven ? "even" : "odd"}.`}</div>
    </div>
  );
}

といった感じになりますかね?

state の管理をコンポーネントの外側で行うため、いわゆる Redux っぽい考え方になると思います。

で、個人的におもしろいなーと思うのが、useEffect が不要になっていることですね。

今までの書き方だと count の副作用によって isEven の値を書き換えざるを得なかったのですが。

useReducer の場合 state をオブジェクトで持つことがほとんどだと思いますので、結果的に副作用として働かせる必要がなくなるということですね。

前の state に基づいて次の state を決める必要

ちょっと表現があいまいでわかりづらいですが、公式ドキュメントの useState ところに書いてある補足のことだと思っています。

関数型の更新

クラスコンポーネントの setState メソッドとは異なり、useState は自動的な更新オブジェクトのマージを行いません。この動作は関数型の更新形式をスプレッド構文と併用することで再現可能です:

const [state, setState] = useState({});
setState((prevState) => {
  // Object.assign would also work
  return { ...prevState, ...updatedValues };
});

別の選択肢としては useReducer があり、これは複数階層の値を含んだ state オブジェクトを管理する場合にはより適しています。

複数階層の値を含んだ state オブジェクト というのが引っかかりやすいポイントだと思いますが。

スプレッド構文による複製はシャローコピー、つまりネスト 1(= 1 段階の深さ)のみ行われます。(スプレッド構文

したがって 複数階層の値を含んだ state オブジェクトuseState を用いて更新する場合、コンポーネントに複雑な更新ロジックを持たせることになってしまいます。

となれば、無理にコンポーネントの内部で解決せず、useReducer を使って更新ロジックを外出しにしたほうが良い、ということなのかなーと思っています。

useReducer による最適化

また、useReducer を使えばコールバックの代わりに dispatch を下位コンポーネントに渡せるようになるため、複数階層にまたがって更新を発生させるようなコンポーネントではパフォーマンスの最適化にもなります。

文脈通りに受け取ると dispatch 関数を props として流し込んでしまいそうになりますが、これは間違った解釈です。

我々は個別のコールバックを props として渡すのではなく、コンテクスト経由で dispatch を渡すことを推奨しています。

https://ja.reactjs.org/docs/hooks-faq.html#how-to-avoid-passing-callbacks-down

大きなコンポーネントツリーにおいて我々がお勧めする代替手段は、useReducer で dispatch 関数を作って、それをコンテクスト経由で下の階層に渡す、というものです。

手順としては以下の通りですね。

  1. useReducer を呼び出し、dispatch 関数を定義する
  2. createContext を呼び出し、Providerdispatch 関数を流し込む
  3. 子コンポーネントで useContext を呼び出し、dispatch 関数を取得する

この方法によるメリットも書かれていますね。

dispatch のコンテクストは決して変わらないため、dispatch だけを使うコンポーネントは(アプリケーションの state も必要でない限り)再レンダーする必要がなくなります。

useReducer によって取得される dispatch 関数は不変になるため、パフォーマンスの向上が望められるってことですね。

ただし state もコンテキストに渡したい場合は注意が必要なようです。

もしもコンテクストを使って state も渡すことにする場合は、2 つの別のコンテクストのタイプを使ってください

とはいえ、個人的にはあまり推奨したいやり方ではないです。

単純に Redux の useDispatchuseSelector を好き勝手に呼び出したら大変なことになるよね?って話と同じなわけで。

我々が見たところ、ほとんどの人はコンポーネントツリーの各階層で手作業でコールバックを受け渡ししていく作業が好きではありません。それはより明示的ではありますが、面倒な『配管工事』をしている気分になることがあります。

と書かれている通り、明示的ではなくなりますし保守難易度がぐっと上がります。

基本的には愚直かつ明示的に props で流し込んでいきましょう。


そんな感じみたいです、やはり扱うのはかなり難易度が高そうですね。

他にも遅延初期化に関するドキュメントもあるようですが、そこまで頻繁に扱うものでもないと思うのでスルーします。

公式ドキュメントでは比較的 useReducer に関するメリットばかり書かれていましたが(当たり前だけど)、個人的な経験としてはデメリットも結構大きいです。


reducer をどこに定義するのか

useState と異なり、reducer はコンポーネントの外側に定義する必要があります。

となると reducer ってどこに書けば良いのか、個人的にはかなり難しいよなーと。

腕の見せ所になりそうな気はしますね。

context も連発させるものじゃない

dispatch 関数を context で流し込めば良いよ!」って書かれていますが、実際問題そんな簡単な話ではないです。

小規模なプロジェクトであればなんとでもなりますが、中規模〜大規模なプロジェクトになってくるとやはりコンポーネント設計が重要です。

そこで子コンポーネントに対して context で流し込むといった手法を使っていると、もうもうもう保守なんてまったくできなくなります。

そういった記述をしているプロジェクトを何度も見てきましたが、本当にやめましょう。

基本は props でバケツリレー、これは絶対です。

基本は useState を使えば良い

調べる前は『state にオブジェクトを扱いたいケースでは useReducer を使えば良いのかな?』と思っていたんですが、そんな感じでもない印象を受けました。

ぶっちゃけシャローコピーで解決が十分なケースも多いと思いますし、基本は useState で良さそうです。

となると useReducer の使いみちって、フロントでよほど複雑な state と更新ロジックを持つケースに限定されそうですよね。


最後に、『個人的にこういうケースでは useReducer は使えるかも』というのを書いておきます。

  • カスタム hooks 内で呼び出す
  • _app のようなもっとも大枠のコンポーネントから流し込む(認証系とかで使えそう)
  • 呼び出し回数が比較的少ないグローバルなコンポーネントで受け取る(ヘッダーやメニューなど)
  • presentational component において UI に関する複雑な state を扱う

公式以外の日本語のドキュメントも色々と読んでみましたが、正直どれもあまりピンと来ず…。

英語ではありますが An Easy Guide to React useReducer() Hook という記事がもっともわかりやすかったです。

結局 useReducer は Redux だなーと強く感じさせられました、あまり良い使いどころはない印象です。

繰り返しになりますが、下手に書くと保守難易度が上がっていく一方になってしまうので基本は useState で十分です。

とはいえ便利な hooks であるのも間違いないので、自分もうまいこと使ってみたいなーと思った今日このごろです。