React.useMemoの配列にはstate値も必要


React Hooksを元気に使っていたのですが、useMemoで必要以上にメモされるというトラブルが起きてしまいました。

React.useMemoとは

Reactのrender関数内で、その場で作った無名関数を使うと、renderのたびに関数が再生成されてしまって、不必要に下位コンポーネントのrenderが必要となってしまいます。また、配列の絞り込みなど複雑な操作を毎回行うと、そのコストもかさみます。

そこで使えるHookが、React.useMemoです。useMemo(() => 欲しい値, [依存する変数の配列])のようにすることで、依存する変数の配列が変化したときだけ無名関数を実行して欲しい値を生成するようになります。

なお、コールバック関数の場合、通常「コールバック関数を作るコストの削減」より「関数を不必要に再生成しない」ということが求められますので、コールバック関数を直接指定する形のReact.useCallback(() => {...}, [依存する変数の配列])というバージョンもあります。

依存する変数について

最後に「依存する変数の配列」を指定しますが、これらのうちどれかが変更されると、値の再生成が行われます。

  • 内部から(直接・間接を問わず)呼び出している、React.useStateの更新関数
  • 内部から(直接・間接を問わず)参照している、propsの値

などは比較的わかりやすいかと思いますが、実は「内部から(直接・間接を問わず)参照しているReact.useStateの値変数」も、ここに入れる必要があります。

入れ忘れた失敗

React.useStateの値変数を書かずにReact.useCallbackを適用したところ、取得するstate値が最初の値から全く変化しない、という事態に見舞われました。

「実行のたびにstate値を参照するはずなのに」と思ったのですが、よくよく考えればコールバックから参照する変数がクロージャを生成するために、キャッシュされた関数を使った時にはキャッシュ時のstate値を参照する結果となって、今の値が取れない、ということになってしまうのでした。

function HookComponent(){
  const [value, setValue] = React.useState('');

  const handleChange = React.useCallback(
    e => setValue(e.target.value), [setValue]
  );

  // valueを書き換え対象に入れていないので、前のvalueを掴んだままとなる 
  const handleClick1 = React.useCallback(
    () => alert(value), []
  );

  // valueの変化に追随して、関数も作り直される
  const handleClick2 = React.useCallback(
    () => alert(value), [value]
  );

  // Hook定義完了

  return(
    <div>
      <input type="text" value={value} 
        onChange={handleChange}
      />
      <br />
      <button type="button"
        onClick={handleClick1}
      >
        valueなし
      </button>
      <button type="button"
        onClick={handleClick2}
      >
        valueあり
      </button>
    </div>
  );
}

CodePenで動かしてみました

See the Pen yZxvjv by Jkr2255 (@jkr2255) on CodePen.