🔄

【React】stateに変更がない場合のレンダリング

2024/02/16に公開

概要

Reactのstateを使用している時に、stateの値が変わっていないはずなのに再レンダリングされていることに気づいたので、調査してまとめました。

コード例

以下のように、例としてstate01の値を加算していくコードについて考えます。
レンダリングのタイミングをログで出しています。codeSandBoxも貼ってるので、再レンダリングのタイミングを思い出しながら、ボタンをクリックして遊んでみてください。

const Component = () => {
  console.log("render");
  
  const [state, setState] = useState(0);
  const data = [0, 1];
  
  return (
    <>
      {data.map((datum) => (
        <button onClick={() => setState((state) => state + datum)}>
          + {datum}
        </button>
      ))}
      <p>state: {state}</p>
    </>
  );
};  

再レンダリングが起こるタイミング

codeSandBoxで試してもらえば分かりますが、レンダリング(consoleにrenderが表示される)は以下のタイミングだと分かります。

  1. 一番初めのレンダリング
  2. +1をクリックしてstateの値が増えた時
  3. +1をクリックしたあとに+0をクリックした時

1.2. については、Reactの中では当たり前といえるレンダリングの原因だと思います。
3.についてはどうでしょうか。+1をクリックするとstateの値が変わるため再レンダリングされるのは分かります。その後に+0をクリックしてもstateの値は0が足されるだけなので、結局stateの値が変わらず再レンダリングは起こらないはずです。
しかし、今回の場合は再レンダリングが発生しています。

原因

Reactのissueにて訪ねてみると、以下のような返信が届きました。

when you first click "unchanged state" the last reducer rendered was setState(state => state +1), so when we compare it to the result of setState(state => state + 0), they're different . When you click again, the last reducer was setState(state => state + 0), so they match and we can bail out.
[Bug: Parent rerenders unnecessarily on Child-to-Parent setter call, while child rerenders only when changes occur. · Issue #28287 · facebook/react](https://github.com/facebook/react/issues/28287#:~:text=when you first,closed over props.

まず、前提としてstateset関数が呼び出されると、そのコンポーネントは再レンダリングが予約されます。その時にもし、set関数のコールバック関数に変更があった場合は何もせず、再レンダリングを実行しますが、set関数のコールバック関数に変更が無かった場合は予約を解除することによって、再レンダリングを実行しない仕組みになっています。
ただし、stateの値が同じであっても最初の1回目のみは、set関数中のコールバック関数で使われているpropsstatehook等の変更が反映されていないときがあり、本当に同じ値かを確かめるために、もう一度レンダリングし、hook等を再実行した上で同じ値である事を確認します。2回目以降でこのset関数のみが呼び出された場合は、他のpropsstate等は変更されていないはずなので確認のレンダリングは必要なくなります。

今回の例でいうと、+1ボタンをクリックするとsetState(state => state + 1)のコールバック関数の返り値が1になり、stateの値は1になります。次に+0をクリックすると、setState(state => state + 0)のコールバック関数の返り値は1になり、同じ値が続くのが1回目なので確認のためにレンダリングが走り、consoleにrenderが表示されます。もう一度+0をクリックすると、コールバック関数の返り値は1になり、同じ値が2回続き、このset関数のみ呼び出していて他のpropsstate等に変更はないはずなのでレンダリングが走らず、consoleにrenderが表示されません。

最後に

実際、値が変わらないset関数を使用することや、レンダリングのタイミングによってバグが発生する事は少ないかもしれませんが、知っておいて損は無い知識だと思います。周りに自慢しましょう!💪
今回の調査で様々なことが分かりましたが、set関数stateに値が入るタイミングやReactの再レンダリングの予約から完了までのフェーズがどうなっているか等は実装を読まない限り分かりませんでした。
もし、気になったらReactの実装等を読んでコメントとかで詳しく教えてください。

ユニフォームネクスト株式会社

Discussion