React useCallback を使ってもあまり意味がないと感じたケースについて

React の hook の一つである useCallback は、子コンポーネントに渡す関数をキャッシュ化することで無駄なレンダーを避けるために使います。

useCallbak を使わない場合、コンポーネントのレンダーの度に新しい関数が作成され、前のレンダーの時に作成された関数と、次のレンダーで作成された関数は等価ではないため、React.memo で子コンポーネントをメモ化している時でも無駄なレンダーが走ってしまいます。

React.memo は、React.memo(MyComponent) のように使い、MyComponent に渡される props が一致していればレンダーをしなくなるのですが、上記のように props の中に関数があると、その関数が前回と等価ではないため、再レンダーが防げなくなります。

このように、React.memo をうまく機能させるために、同じ関数を props に渡したい時に使うのがメリットのある使い方である useCallback ですが、以下のように使った場合あまり意味がありません。

import React, {useState, useCallback} from "react";
import ChildComponent from "./ChildComponent";

const ParentComponent = () => {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []);

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
      <ChildComponent onClick={handleClick} />
    </div>
  )
};

export default ParentComponent;
import React from "react";

const ChildComponent = ({onClick}) => {
  console.log('render child component');
  return (
    <div onClick={onClick}>child</div>
  )
};

export default ChildComponent;

この例では ParentComponent で useCallback を使い、ChildComponent に渡す handleClick をキャッシュ化しています。

一見問題なさそうですが、ChildComponent は ParentComponent がレンダーされると再レンダーされるので意味がありません。

ParentComponent の count が変わってレンダーされると、ChildComponent も再レンダーされます。

一応、ParentComponent 内の handleClick 関数の生成は再レンダーの度に行われなくなるのですが、useCallback の第二引数とuseCallbackの第一引数の関係を常に一緒にしておく必要があり(どういったときに関数の生成をスキップするかを意識する必要がある)、コードの複雑性が増すのでそこまでの恩恵は感じられません。

ChildComponent の最後の行を以下のように変更することで、ChildComponent の再レンダーを防ぐことはできますが、ChildComponen は巨大なコンポーネントというわけでもなく、キャッシュ化のメリットはそこまで感じられませんでした。むしろ、先ほどの useCallback の第二引数を常に意識しないといけないデメリットの方が大きいように感じます。

export default React.memo(ChildComponent);

巨大なコンポーネントの再レンダーを防ぎたいが、そのコンポーネントのpropsに関数を渡しているので毎回再レンダーされて困っている、という時に使えたらいいのかなと感じました。