ReactのuseCallback徹底ガイド──依存配列・再レンダリング制御・パフォーマンス最適化の基本と応用

React

はじめに

Reactの関数コンポーネントで副作用や再レンダリング制御を扱う際、useCallback フックはしばしば「面倒くさい」「よくわからない」と敬遠されがちです。しかし、大規模アプリのパフォーマンスチューニングや、子コンポーネントへのコールバック関数渡しを正しく行うためには不可欠な存在です。本記事では基礎から依存配列の書き方、内部動作、注意点、具体的な活用シーンまでを解説します。

useCallbackとは?

useCallback は「関数をメモ化(キャッシュ)する」ための React フックです。コンポーネントが再レンダーされるたびに新しい関数インスタンスを生成すると、子コンポーネントに渡したコールバックが”変化”とみなされ、不要な再レンダーにつながります。これを防ぐのが useCallback です。

const memoizedHandler = useCallback(() => {
  console.log('クリックされました');
}, [依存値1, 依存値2]);
  • 第1引数:メモ化したい関数
  • 第2引数:依存配列。ここに入れた値が変わると新しく関数を作り直す

基本シグネチャと戻り値

function useCallback<T extends (...args: any[]) => any>(
  callback: T,
  deps: DependencyList
): T;
  • T:任意の関数型
  • DependencyListany[] 型の配列
  • 戻り値:渡した callback と同じ型の“メモ化された関数”

内部では React が Hook ごとに前回の callback 参照を保持し、deps が完全一致する限り同じ関数を返します。

依存配列の正しい書き方

依存配列を適切に指定しないと、次のような問題が起きます。

誤り症状
依存配列を空にする ([])最新の外部変数を参照できない (stale closure)
依存を省略 (undefined)毎回新しい関数が作られ、効果なし
必要な依存を含めない古い値を使い続ける

例:カウントを依存に含めないと古い値を参照する

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

  const handler = useCallback(() => {
    console.log(count); // 依存にcountを含めないと常に0が表示
  }, []); 

  return <button onClick={handler}>ログ出力</button>;
}

上記では count が変わっても handler 内の count は初回のまま。依存配列には常に最新のcountを正しく指定しましょう。

useCallbackとuseMemoの違い

  • useCallback:関数をメモ化する
  • useMemo:関数の戻り値(任意の値)をメモ化する
const memoizedValue = useMemo(() => computeExpensive(a, b), [a, b]);
const memoizedHandler = useCallback(() => doSomething(a, b), [a, b]);

useCallback(fn, deps)useMemo(() => fn, deps) のエイリアスと考えることもできますが、可読性の観点から関数キャッシュには useCallback を使うのが一般的です。

  1. コンポーネント初回レンダー
  2. useCallback が新しい関数を生成し返却
  3. 次回レンダー
    • deps が同じ → 前回の関数を再利用
    • deps が変わった → 新しい関数を生成
  4. 子コンポーネントへ渡す際、関数参照の変化を検出してレンダー制御できる

この仕組みを利用し、React.memo と組み合わせることで子コンポーネントの再レンダーを抑制できます。

子コンポーネントの最適化例

const Child = React.memo(({ onAction }) => {
  console.log('Child render');
  return <button onClick={onAction}>アクション</button>;
});

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

  const handleAction = useCallback(() => {
    console.log('Action!');
  }, []); // 依存なし → 再レンダー時も同じ参照

  return (
    <>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>増やす</button>
      <Child onAction={handleAction} />
    </>
  );
}
  • Parent が再レンダーされても handleAction は同じ参照
  • Child は Props が変わらないとレンダーされないため高速

使わないほうがいいケース

  • 軽量コンポーネント/関数
    関数の生成コストよりReactのレンダーコストのほうが低い場合
  • 短命な値
    毎回異なる依存配列を生成すると逆効果
  • 過度のメモ化
    コードが複雑になり可読性や保守性が低下

判断基準
パフォーマンス問題が「本当に関数参照の変化」に起因しているかプロファイラで確認しましょう。

複数フックとの連携パターン

useCallback + useEffect

エフェクト内で関数を参照する場合、依存配列に含めないと stale closure が起きます。

function Search({ query }) {
  const [results, setResults] = useState([]);
  const fetchResults = useCallback(async () => {
    const res = await fetch(`/api?q=${query}`);
    setResults(await res.json());
  }, [query]);

  useEffect(() => {
    fetchResults();
  }, [fetchResults]); // fetchResults を正しく依存に含める

  return /* 結果表示 */;
}

useCallback + useMemo

関数による計算結果をさらにメモ化したい場合

const expensiveCompute = useCallback((x) => heavyCalc(x), []);
const memoizedResult = useMemo(() => expensiveCompute(input), [input, expensiveCompute]);

useCallback でメモ化された関数をテストする際は、renderHookact を使うと楽です。

コールバック関数のテスト

useCallback でメモ化された関数をテストする際は、renderHookact を使うと楽です。

import { renderHook, act } from '@testing-library/react-hooks';

test('fetchResults が更新時に変化する', async () => {
  const { result, rerender } = renderHook(
    ({ query }) => useCallback(/*…*/, [query]),
    { initialProps: { query: 'a' } }
  );
  const first = result.current;
  rerender({ query: 'b' });
  expect(result.current).not.toBe(first);
});

パフォーマンス計測とベンチマーク

  • Profiler API で実際のレンダー回数を計測
  • React DevTools のProfilerタブでコールバック関数生成の影響を見る
  • useCallback 無し / 有りでどれだけ再レンダーが減るかを数値で比較

よくある誤解とQ&A

  1. 「depsが空なら一度だけ実行される」
    useCallback では「関数参照が一度だけ生成される」。 useEffect([], …) とは別。
  2. 「関数を渡すだけで最適化される」
    → 子コンポーネントも React.memo などで参照比較しなければ効果なし。
  3. 「常に使えば良い」
    → 過度のメモ化は逆効果。必要な箇所だけに絞りましょう。

まとめ

useCallback は関数インスタンスの再生成を防ぎ、再レンダーを制御する強力なフックです。依存配列の正確な管理、React.memo との併用、過度なメモ化の回避などポイントを押さえれば、アプリ全体のパフォーマンスが向上します。まずはパフォーマンスプロファイリングを行い、本当に必要な箇所だけに導入してみてください。

コメント