useCallback は関数定義を依存値が変わるまで再利用する Hook です。memo でメモ化された子コンポーネントへコールバックを渡す場合に、props の参照が毎回変わるのを避けられます。
memo 化した子と組み合わせる
import { memo, useCallback, useState } from 'react';
const SaveButton = memo(function SaveButton({ onSave }: { onSave(): void }) {
return <button onClick={onSave}>保存</button>;
});
function Editor({ documentId }: { documentId: string }) {
const [title, setTitle] = useState('');
const save = useCallback(() => saveDocument(documentId), [documentId]);
return <><input value={title} onChange={(event) => setTitle(event.target.value)} /><SaveButton onSave={save} /></>;
}
この例で documentId が変われば保存先も変わるため、新しい save が必要です。依存配列は「いつ関数の振る舞いが変わるか」の一覧です。必要な値を外すと、見た目は更新されても古い documentId へ保存するようなバグになります。
title だけが変わっても save の参照は変わらないため、SaveButton は再レンダーを省略できます。
この最適化が成り立つのは、子が memo で props を浅く比較しているからです。useCallback があっても、別の props が毎回新しいオブジェクトなら子は再レンダーします。親子の props 全体と、子の描画コストを一緒に見て判断します。
使う判断
useCallback は useMemo(() => fn, deps) と同じ目的の専用 API です。子が memo 化されていない、または関数参照が依存関係に影響しないなら効果はありません。まず state を局所化し、不要な Effect を減らす設計を優先します。
落とし穴
依存値を空配列にして古い props や state を閉じ込めないでください。前の state を基準にする更新は setState((current) => ...) の updater にすると、不要な依存を減らせます。
Effect 内で使う関数を安定させたいときも、まずその関数を Effect 内に置けないかを考えます。依存配列の警告を消すためだけに useCallback を増やすと、依存関係がかえって読みづらくなります。性能問題は React DevTools Profiler などで確認してから最適化します。
カスタム Hook の返り値
カスタム Hook が関数を返し、その利用者がその関数を Effect の依存にする場合は useCallback が有用なことがあります。ただし、利用者へ公開する API の意味を先に決めます。「いつ参照が変わるか」が利用者にとって意味を持たないなら、無理に安定性を保証する必要はありません。
まとめ
useCallback は関数を速くする Hook ではなく、参照の同一性を保つ Hook です。メモ化した子で測定できる効果があるときだけ使います。