「子コンポーネントが毎回再レンダリングされるんだけど…」
Reactで開発していて、React DevToolsのプロファイラを開いたら子コンポーネントが毎回光ってる。React.memoで囲んだのに効いてない。「なんで?」と調べてみたら、親から渡しているコールバック関数が毎回新しいインスタンスになっているのが原因だった。こんな経験、ありませんか?
この問題を解決するのがuseCallbackです。本記事では、useCallbackの基本概念から、実際に効果があるケース・ないケース、useMemoとの違いまでをコード例付きで解説します。
useCallbackとは? ― 関数をメモ化するReactフック
useCallbackは、関数の参照を依存配列が変わるまで保持する(メモ化する)Reactフックです。
Reactのコンポーネントは、stateやpropsが変わるたびに再レンダリングされます。その際、コンポーネント内で定義された関数は毎回新しいインスタンスとして生成されます。
【useCallbackなし】
レンダリング1回目: handleClick = function A(新規生成)
レンダリング2回目: handleClick = function B(新規生成 ← Aとは別物)
レンダリング3回目: handleClick = function C(新規生成 ← またまた別物)
【useCallbackあり】
レンダリング1回目: handleClick = function A(新規生成)
レンダリング2回目: handleClick = function A(同じ参照を再利用)
レンダリング3回目: handleClick = function A(同じ参照を再利用)関数の中身は同じでも、JavaScriptでは参照が違えば別物と判定されます。これがReact.memoをすり抜ける原因です。
なぜ必要? ― 再レンダリングで関数が作り直される問題
具体的なコードで問題を見てみましょう。
// ❌ useCallbackなし:子が毎回再レンダリングされる
function Parent() {
const [count, setCount] = useState(0);
// レンダリングのたびに新しい関数が生成される
const handleClick = () => {
console.log('clicked');
};
return (
<>
{count}
{/* handleClickが毎回新しい → React.memoが効かない */}
>
);
}
const ExpensiveChild = React.memo(({ onClick }: { onClick: () => void }) => {
console.log('ExpensiveChild rendered'); // ← 毎回ログが出る
return ;
});親のcountが変わるたびにhandleClickが新しく生成され、ExpensiveChildは「propsが変わった」と判断して再レンダリングされます。React.memoで囲んでいるのに意味がない状態です。
基本的な使い方
上のコードをuseCallbackで修正します。
// ✅ useCallbackあり:子の不要な再レンダリングを防ぐ
function Parent() {
const [count, setCount] = useState(0);
// 依存配列が空 → 初回レンダリング時の関数を保持し続ける
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return (
<>
{count}
{/* handleClickの参照が変わらない → React.memoが効く */}
>
);
}構文はuseCallback(関数, 依存配列)。依存配列の値が変わらない限り、同じ関数参照が返されます。
依存配列にstateを含めるケース
関数内でstateを参照している場合は、依存配列に含めます。
const [query, setQuery] = useState('');
// queryが変わった時だけ関数を再生成
const handleSearch = useCallback(() => {
fetchResults(query);
}, [query]);依存配列を空にしたまま中でstateを使うと、古い値を参照し続けるバグ(stale closure)になるので注意。公式ドキュメント(useCallback – React公式リファレンス)も確認しておきましょう。
useCallbackが効くケース・効かないケース
useCallbackは万能ではありません。使うべき場面と、使っても意味がない場面をはっきり区別しましょう。
| 場面 | useCallback | 理由 |
|---|---|---|
React.memoの子にコールバックを渡す |
✅ 効く | 参照が安定するのでmemoが正しく動作する |
useEffectの依存配列に関数を入れる |
✅ 効く | 参照が安定するので不要な再実行を防げる |
| 子に渡さないローカル関数 | ❌ 不要 | メモ化のコスト分むしろ遅くなる |
React.memoなしの子にコールバックを渡す |
❌ 不要 | 子がそもそも毎回再レンダリングされるので意味なし |
ポイントは、useCallbackは単体では何もしないということ。React.memoやuseEffectとセットで使って初めて効果がある仕組みです。「とりあえず全部useCallbackで囲む」は、コードが読みにくくなるだけで逆効果です。
useMemoとの違い
useCallbackとuseMemoは混同されがちですが、メモ化する対象が違います。
// useCallback → 「関数そのもの」をメモ化
const handleClick = useCallback(() => {
doSomething();
}, []);
// useMemo → 「関数の実行結果(値)」をメモ化
const sortedList = useMemo(() => {
return heavySort(items);
}, [items]);
// 実は useCallback(fn, deps) は useMemo(() => fn, deps) と同じ| フック | メモ化する対象 | 主な用途 |
|---|---|---|
useCallback |
関数の参照 | 子コンポーネントへのコールバック渡し |
useMemo |
計算結果の値 | 重い計算のキャッシュ、オブジェクト参照の安定化 |
迷ったら「渡すのが関数ならuseCallback、値ならuseMemo」と覚えておけばOKです。
useCallbackの理解をさらに深める関連記事
useCallbackを正しく使いこなすには、Reactのレンダリング最適化やフック全般の理解も欠かせません。
React開発の基礎力を上げる
- リリース前に必ず確認!バイブコーディング&非エンジニア向けWebアプリ安全チェックリスト – パフォーマンス最適化だけでなく、リリース前の品質チェック全般に
- Claude Codeとは?AI搭載のコーディングアシスタントを徹底解説 – useCallbackの適切な配置をAIに相談しながら進める活用法
- GitHub Copilot日本語サイトが正式オープン!Free/Pro/Pro+全プラン徹底比較【2025年】 – メモ化パターンのコード補完にCopilotを活用する選択肢
開発環境・ツール
- コマンドラインの基本と活用方法【初心者エンジニア向け】 – Reactプロジェクトのセットアップやビルドをターミナルから効率的に行う基本
- 「それ、上げちゃダメ!」GitHub管理で絶対守るべきセキュリティルールと対処法 – チーム開発でのコードレビュー時、useCallbackの過不足を指摘する文化づくりに
まとめ:useCallbackは「セットで使って初めて効く」フック
本記事のポイントを整理します。
- useCallbackとは、関数の参照を依存配列が変わるまでメモ化するReactフック
- 必要な理由:コンポーネント再レンダリング時に関数が毎回新しく生成され、
React.memoが効かなくなる問題を解決する - 効く場面:
React.memoの子にコールバックを渡す時、useEffectの依存配列に関数を入れる時 - 効かない場面:子に渡さないローカル関数、
React.memoなしの子へのコールバック - useMemoとの違い:useCallbackは「関数」を、useMemoは「値」をメモ化する
- 依存配列に注意:stateを使う関数は依存配列に含めないとstale closureバグになる
useCallbackは、理解せずに使うと「おまじない」になりがちなフックです。でも仕組みを知れば判断は明快で、「React.memoとセットで使う」「useEffectの依存に入れる関数に使う」、それ以外は不要。このルールだけ覚えておけば、パフォーマンス最適化で迷うことは減るはずです。
