useCallbackが不要なケースを整理する

公開日:

タグ:

  • #Next.js
  • #React

前回の記事で書いた React.memo は「コンポーネントをメモ化」するものでした。今回は useCallback について学んだのでまとめたいと思います。

useCallbackとは何か?

useCallbackを簡単に説明すると**「関数をメモ化するフック」**です。

具体的にいうとコンポーネントで再レンダリングが発生した際に、子コンポーネントのpropsに関数を渡していると、参照が変わることで子コンポーネントでも再レンダリングが発生することがあります。

以下が子コンポーネントのpropsに関数を渡した際に、再レンダリングが発生してしまう事例です。

'use client';
import { memo, useCallback, useState } from 'react';

const Items = [
  { id: 1, name: 'Apple', price: 300 },
  { id: 2, name: 'Orange', price: 250 },
  { id: 3, name: 'Banana', price: 100 },
];

export default function Page() {
  console.log('Page rendered');

  const [selectItem, setSelectItem] = useState('');

  return (
    <div>
      <div>{selectItem ? `Selected: ${selectItem}` : 'No item selected'}</div>
      <ItemList items={Items} onSelect={(name) => setSelectItem(name)}></ItemList>
    </div>
  );
}

const ItemList = memo(function ({
  items,
  onSelect,
}: {
  items: typeof Items;
  onSelect: (name: string) => void;
}) {
  console.log('ItemList rendered');
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          <button onClick={() => onSelect(item.name)}>
            {item.name}: ${item.price}
          </button>
        </li>
      ))}
    </ul>
  );
});

ItemList.displayName = 'ItemList';

上記の場合、 Page コンポーネント内の onSelect で渡している関数が、実行されるたびに毎回新しい関数(別の参照)が渡されていると判断されて、 memo を使用していても ItemList の再レンダリングが行われてしまいます。

useCallbackで関数をメモ化する

先程のコードのonSelectで渡していた関数をuseCallbackを使用した関数に置き換えます。

  const handleSelect = useCallback((name: string) => {
    setSelectItem(name);
  }, []);

上記のように、渡していた関数をラップする形で使用します。

第二引数には、コールバック内で参照する値(state/props/変数など)を並べます。

ここでは setSelectItem を使っていますが、state setter は参照が安定しているため [] でも挙動上は問題になりにくいです(気になる場合は [setSelectItem] と書いてもOK)。

'use client';
import { memo, useCallback, useState } from 'react';

const Items = [
  { id: 1, name: 'Apple', price: 300 },
  { id: 2, name: 'Orange', price: 250 },
  { id: 3, name: 'Banana', price: 100 },
];

export default function Page() {
  console.log('Page rendered');

  const [selectItem, setSelectItem] = useState('');
  const handleSelect = useCallback((name: string) => {
    setSelectItem(name);
  }, []);

  return (
    <div>
      <div>{selectItem ? `Selected: ${selectItem}` : 'No item selected'}</div>
      <ItemList items={Items} onSelect={handleSelect}></ItemList>
    </div>
  );
}

const ItemList = memo(function ({
  items,
  onSelect,
}: {
  items: typeof Items;
  onSelect: (name: string) => void;
}) {
  console.log('ItemList rendered');
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          <button onClick={() => onSelect(item.name)}>
            {item.name}: ${item.price}
          </button>
        </li>
      ))}
    </ul>
  );
});

ItemList.displayName = 'ItemList';

このように onSelect の参照を安定させることで、親が再レンダリングしても ItemList の props が変わらず、React.memo によって再レンダリングがスキップされます。

useCallbackがなくてもReact.memoが正しく機能する?

先程のコードですが、実は useCallbackがなくても React.memo が正しく機能させる方法があります。

'use client';
import { memo, useState } from 'react';

const Items = [
  { id: 1, name: 'Apple', price: 300 },
  { id: 2, name: 'Orange', price: 250 },
  { id: 3, name: 'Banana', price: 100 },
];

export default function Page() {
  console.log('Page rendered');

  const [selectItem, setSelectItem] = useState('');

  return (
    <div>
      <div>{selectItem ? `Selected: ${selectItem}` : 'No item selected'}</div>
      <ItemList items={Items} onSelect={setSelectItem}></ItemList>
    </div>
  );
}

const ItemList = memo(function ({
  items,
  onSelect,
}: {
  items: typeof Items;
  onSelect: (name: string) => void;
}) {
  console.log('ItemList rendered');
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          <button onClick={() => onSelect(item.name)}>
            {item.name}: ${item.price}
          </button>
        </li>
      ))}
    </ul>
  );
});

ItemList.displayName = 'ItemList';

上記のように setSelectItem を直接渡すことで ItemList の再レンダリングは items が更新されない限り行われません。

これは、 setState は参照が変わらない安定した関数(変更されることがない)のため React.memo が正しく反映されます。このようにuseCallbackも必要な場合と必要でない場合があるので注意が必要です。以下に、必要なケース、必要ではないケースをまとめました。

useCallbackが必要なケース

1. propsとして子に関数を渡す場合

const handleClick = () => {
  console.log("clicked");
};

<Child onClick={handleClick} />
  • Child が React.memo されている
  • 関数の参照が変わると再レンダリングされる

このような場合、関数の参照を安定させるために useCallback が役立ちます。

2. useEffectの依存配列に関数が入る場合

useEffect(() => {
  fetchData();
}, [fetchData]);
  • 関数が毎回変わる
  • effectが毎回再実行される

上記の場合はuseCallbackでメモ化する意味があります。

useCallbackが不要なケース

1. propsとして関数を渡していない場合

const handleClick = () => {
  setOpen(true);
};
  • 自分のコンポーネント内だけで使う場合
  • 参照が変わっても困らない場合

上記のような場合はuseCallbackを使用する必要はありません。

2. useEffect内に閉じている関数

useEffect(() => {
  const handleClick = () => {};
  document.documentElement.addEventListener('click', handleClick);
  return () => {
	  document.documentElement.removeEventListener('click', handleClick);
  };
}, []);

上記のように、useEffect内で定義した関数であれば useCallbackを使用する必要はありません。

3. 子コンポーネントがmemo化していない場合

<Child onClick={handleClick} />
  • コンポーネントがメモ化されてないので毎回再レンダリングされる
  • 参照を固定しても意味がない

useCallbackを使用しても子コンポーネントがメモ化されていない場合は再レンダリングが走ってしまうので意味がありません。

※)軽い処理の関数

const toggle = () => setOpen(v => !v);
  • 再生成コストがほぼゼロ
  • useCallbackの管理コストの方が高い

少しズレますが、単純な関数の生成であれば、useCallbackを使って管理する方がコードも煩雑になりコストがかかるので許容して良い範囲ではないかと感じました。

まとめ

React.memo同様ですが、useCallbackも同様に、使用が必要な部分を見極める必要があると感じました。また使用する前に今の実装が整理されているか改めて確認することも大事だと思います。

参考サイト

一覧へ戻る