T-CREATOR

useCallbackとは?再レンダリングを防ぐための基本と使い方をわかりやすく解説

useCallbackとは?再レンダリングを防ぐための基本と使い方をわかりやすく解説

1. useCallbackが必要となる背景

Reactの関数コンポーネントは、ステートやプロップスが更新されるたびに再レンダリングが発生します。 その際、関数内部で定義されたコールバック関数も毎回新しく生成されます。

このような仕組みにより、親から関数を props 経由で渡された子コンポーネントは、関数の参照が毎回変わるため、React.memoなどでメモ化しても、毎回再レンダリングされてしまうことがあります。

tsxfunction Parent() {
  const handleClick = () => {
    console.log('clicked');
  };

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

このようなコードでは、Parentが再レンダリングされるたびに handleClick が新しくなり、Childの再レンダリングを引き起こしてしまいます。

2. useCallbackの基本的な使い方

tsxconst memoizedCallback = useCallback(() => {
  // 実行される処理
}, [依存配列]);

useCallback は、依存配列の中身が変わらない限り、同じ関数インスタンスを返します。 これにより、関数の参照が固定され、React.memoやuseEffectなどの依存関係として扱う際に非常に有効です。

例えば、次のように setCount を使ってカウントアップする処理を useCallback でメモ化できます。

tsxconst increment = useCallback(() => {
  setCount((prev) => prev + 1);
}, []);

依存配列が空なので、increment は初回のマウント時に一度だけ定義され、以降のレンダリングでも同じ関数が再利用されます。

3. React.memoとの併用とメリット

React.memoは、propsが変更されない限り再レンダリングを抑えるHOC(高階コンポーネント)です。 しかし、関数が毎回新しく渡されると、それだけで変更と見なされてしまいます。

以下のような例を見てみましょう。

tsxconst Button = memo(({ onClick }: { onClick: () => void }) => {
  console.log('Button rendered');
  return <button onClick={onClick}>Click Me</button>;
});

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

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

  return (
    <>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <Button onClick={handleClick} />
    </>
  );
}

このように、useCallback を使えば、handleClick の参照は維持され、Button コンポーネントの不要な再レンダリングが防げます。

4. 実践ユースケース

フォームのバリデーション関数

tsxfunction Form() {
  const [value, setValue] = useState('');

  const validate = useCallback((input: string) => {
    return input.length >= 5;
  }, []);

  return (
    <div>
      <input
        value={value}
        onChange={(e) => setValue(e.target.value)}
        style={{ borderColor: validate(value) ? 'green' : 'red' }}
      />
    </div>
  );
}

毎回 validate を新しく生成しないことで、バリデーションロジックを他の副作用から切り離すことができます。

リスト描画時のクリックハンドラ

tsxconst ListItem = memo(({ id, onSelect }: { id: number; onSelect: (id: number) => void }) => {
  console.log('ListItem rendered:', id);
  return <li onClick={() => onSelect(id)}>Item {id}</li>;
});

function List({ items }: { items: number[] }) {
  const handleSelect = useCallback((id: number) => {
    alert(`Selected item: ${id}`);
  }, []);

  return (
    <ul>
      {items.map((item) => (
        <ListItem key={item} id={item} onSelect={handleSelect} />
      ))}
    </ul>
  );
}

このようなリストでは、ハンドラの参照が変わらなければ、個々の ListItem は無駄にレンダリングされることがなくなります。

Context.Provider の value に関数を含める

tsxconst AppContext = createContext<{ onAction: () => void }>({ onAction: () => {} });

function AppProvider({ children }: { children: ReactNode }) {
  const onAction = useCallback(() => {
    console.log('context action');
  }, []);

  const contextValue = useMemo(() => ({ onAction }), [onAction]);

  return <AppContext.Provider value={contextValue}>{children}</AppContext.Provider>;
}

useCallback → useMemo の順でメモ化することで、Providerのvalueが変わらず、消費側コンポーネントの再レンダリングも抑制できます。

5. カスタムフックによる抽象化

複数の場所で useCallback を使っていると、ロジックが散らかりやすくなります。 そうした場合は、共通の処理をカスタムフックに抽出して再利用性を高めましょう。

tsxfunction useIncrement(setter: React.Dispatch<React.SetStateAction<number>>) {
  return useCallback(() => setter((prev) => prev + 1), [setter]);
}

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

  return <button onClick={increment}>カウント: {count}</button>;
}

6. よくある疑問と回答

疑問回答
setStateは依存配列に入れる?基本は不要(安定しているため)
関数の処理が速くなる?処理自体は変わらず、参照の維持が目的です
useCallbackの使いすぎは?パフォーマンス改善どころか逆に複雑化します

7. テストへの活用

React Hooksのテストでは、関数の参照が変わらないことを確認するケースもあります。 useCallback を使っていると、hook単体のテストが書きやすくなります。

tsxtest('useIncrementは常に同じ関数を返す', () => {
  const { result, rerender } = renderHook(() => useIncrement(jest.fn()));
  const first = result.current;
  rerender();
  const second = result.current;
  expect(first).toBe(second);
});

8. useRefなど他のフックとの連携

useCallback 単体では最新の状態を参照することができません。 非同期処理やイベントリスナなどと併用する場合は useRef を組み合わせると良いです。

tsxfunction useStableCallback(callback: () => void) {
  const ref = useRef(callback);
  useEffect(() => {
    ref.current = callback;
  }, [callback]);

  return useCallback(() => ref.current(), []);
}

9. useMemoとの違い

比較項目useCallbackuseMemo
主目的関数のメモ化値のメモ化
返り値関数任意の値
使用例propsに渡すコールバック関数高コスト計算の結果の再利用

10. まとめと使いどころチェック

  • 関数を props に渡す場面では useCallback を使う
  • React.memo や useEffect などと連携して副作用の最小化
  • useRef や useMemo との併用も考慮
  • 冗長な useCallback の使用は避け、必要な場面だけ使う

正しく使えば、Reactアプリのパフォーマンスと保守性は大きく向上します。

11. 公式リソースリンク

12. 最後に

React開発では「不要な再レンダリングを防ぐ」ことが、体感パフォーマンスとスムーズなユーザー体験に直結します。 そのためにも useCallback の正しい理解と応用は欠かせません。

何でも useCallback にすればいいわけではなく、適切な場所で使う判断力が必要です。 この記事の内容を踏まえて、実際のアプリケーションの中で「ここに使うべきか?」を意識しながら実装していくことが、Reactスキルの向上につながります。

記事Article

もっと見る