useCallback

useCallback で関数参照を保ち、メモ化した子への不要な再レンダーを避ける方法を学ぶ。

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 です。メモ化した子で測定できる効果があるときだけ使います。

Sources