第9回

useCallbackで関数をメモ化:React.memoと組み合わせた最適化

ReactのuseCallbackを解説します。関数のメモ化・React.memoとの組み合わせ・useEffectの依存配列での活用を実例で解説。useMemoとの違いと使い分けも明確にします。

·13分で読める
たける
たける `useMemo` と `useCallback` の違いがいつも混乱します。どっちをいつ使えばいいんですか?
りこ
りこ useMemoは値をメモ化、useCallbackは関数をメモ化。`useCallback(fn, deps)` は `useMemo(() => fn, deps)` と同義。関数をpropsとして子コンポーネントに渡すときはuseCallback。

useCallbackとは

useCallback関数をメモ化するHookです。依存配列の値が変わらなければ、前回と同じ関数参照を返します。

const memoizedFn = useCallback(() => {
  doSomething(a, b);
}, [a, b]);  // a か b が変わったときだけ新しい関数を作る

useMemo との違い:

  • useMemo計算結果(値)をメモ化する
  • useCallback関数自体をメモ化する

なぜ関数をメモ化するのか

Reactコンポーネントはレンダリングのたびに関数を再作成します。

function Parent() {
  const [count, setCount] = useState(0);

  // レンダリングのたびに新しい関数が作られる
  const handleClick = () => {
    console.log('clicked');
  };

  return <Child onClick={handleClick} />;
}

これ自体はパフォーマンス問題にはなりません。問題になるのは子コンポーネントが React.memo でラップされている場合です。onClick の参照が毎回変わるため、React.memo による最適化が無効になります。

React.memoとuseCallbackの組み合わせ

// React.memo でラップ:propsが変わらなければ再レンダリングしない
const Button = React.memo(function Button({
  onClick,
  label,
}: {
  onClick: () => void;
  label: string;
}) {
  console.log(`Button "${label}" レンダリング`);
  return <button onClick={onClick}>{label}</button>;
});

function Counter() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  // ❌ useCallbackなし:textが変わるたびにhandleIncrementが再作成される
  //    → Button が再レンダリングされてしまう
  const handleIncrement = () => setCount((c) => c + 1);

  // ✅ useCallbackあり:依存配列が空なので常に同じ関数参照
  //    → textが変わってもButtonは再レンダリングされない
  const handleIncrement = useCallback(() => {
    setCount((c) => c + 1);
  }, []);  // setCount は参照が安定しているため依存不要

  return (
    <div>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <p>カウント: {count}</p>
      <Button onClick={handleIncrement} label="+1" />
    </div>
  );
}

useEffectの依存配列でuseCallbackを使う

コールバック関数を useEffect の依存配列に含めると、毎回新しい関数が作られて無限ループになります。

function DataFetcher({ onSuccess }: { onSuccess: (data: unknown) => void }) {
  // ❌ onSuccess が毎回新しい関数 → useEffect が毎回実行される
  useEffect(() => {
    fetch('/api/data')
      .then((r) => r.json())
      .then(onSuccess);
  }, [onSuccess]);
}

// 親コンポーネントで useCallback を使って安定させる
function Parent() {
  const [result, setResult] = useState(null);

  // ✅ 常に同じ関数参照を渡す
  const handleSuccess = useCallback((data: unknown) => {
    setResult(data);
  }, []);  // setResult は参照が安定しているため依存不要

  return <DataFetcher onSuccess={handleSuccess} />;
}

useCallbackを使わなくてよいケース

useCallback にもメモ化のコストがかかります。むやみに使うと逆効果です。

// ❌ 不要なuseCallback(子がReact.memoでラップされていない)
function Form() {
  const handleSubmit = useCallback(() => {
    // ...
  }, []);

  return <form onSubmit={handleSubmit}>...</form>;  // 普通のフォーム
}

使うべき目安

状況 判断
子が React.memo でラップされている 使う
useEffect の依存配列に渡す関数 使う
イベントハンドラを多数の子に渡す 使う
子が React.memo でラップされていない 使わない
コンポーネント内部だけで使う関数 使わない

実践:検索フォームの最適化

type SearchResult = { id: number; title: string };

const ResultItem = React.memo(function ResultItem({
  result,
  onSelect,
}: {
  result: SearchResult;
  onSelect: (id: number) => void;
}) {
  return (
    <li onClick={() => onSelect(result.id)}>{result.title}</li>
  );
});

function SearchPage() {
  const [query, setQuery] = useState('');
  const [selected, setSelected] = useState<number | null>(null);
  const [results, setResults] = useState<SearchResult[]>([]);

  // selected が変わっても onSelect の参照は変わらない
  const handleSelect = useCallback((id: number) => {
    setSelected(id);
  }, []);  // setSelected は参照が安定

  useEffect(() => {
    if (!query) return;
    fetch(`/api/search?q=${query}`)
      .then((r) => r.json())
      .then(setResults);
  }, [query]);

  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <ul>
        {results.map((r) => (
          <ResultItem key={r.id} result={r} onSelect={handleSelect} />
        ))}
      </ul>
      {selected && <p>選択: {selected}</p>}
    </div>
  );
}

handleSelectuseCallback でメモ化したことで、query が変わって results が更新されても、onSelect の参照が変わらないため各 ResultItem の不要な再レンダリングを防げます。

たける
たける `React.memo` と `useCallback` をセットで使わないと意味がないってどういうことですか?
りこ
りこ React.memoは「propsが変わらなければ再レンダリングしない」という最適化。でも関数を毎レンダリングで新しく作ると参照が変わって「propsが変わった」とみなされる。useCallbackで同じ関数参照を維持してはじめてReact.memoが効く。
なつみ
なつみ UIの実装だと、大きいリストの各行にコールバックを渡すときによく使う。スクロール中に各行が何度も再レンダリングされると体感の重さに出る。React.DevToolsのProfilerで確認してから入れる。

まとめ

  • useCallback は関数参照をメモ化する(useMemo の関数版)
  • 子コンポーネントが React.memo の場合や、useEffect の依存配列に渡す場合に有効
  • 使いすぎはメモ化コストで逆効果になる
  • セットアップの手間を省くには React.memo + useCallback をセットで考える

次の第10回では、Hookを組み合わせてロジックを再利用する カスタムHook を学びます。