第9回
useCallbackで関数をメモ化:React.memoと組み合わせた最適化
ReactのuseCallbackを解説します。関数のメモ化・React.memoとの組み合わせ・useEffectの依存配列での活用を実例で解説。useMemoとの違いと使い分けも明確にします。
·13分で読める
たける
`useMemo` と `useCallback` の違いがいつも混乱します。どっちをいつ使えばいいんですか?
りこ
useMemoは値をメモ化、useCallbackは関数をメモ化。`useCallback(fn, deps)` は `useMemo(() => fn, deps)` と同義。関数をpropsとして子コンポーネントに渡すときはuseCallback。
useCallbackとは
useCallback は関数をメモ化するHookです。依存配列の値が変わらなければ、前回と同じ関数参照を返します。
const memoizedFn = useCallback(() => {
doSomething(a, b);
}, [a, b]); // a か b が変わったときだけ新しい関数を作るuseMemo との違い:
useMemo→ 計算結果(値)をメモ化するuseCallback→ 関数自体をメモ化する
なぜ関数をメモ化するのか
Reactコンポーネントはレンダリングのたびに関数を再作成します。
function Parent() {
const [count, setCount] = useState(0);
// レンダリングのたびに新しい関数が作られる
const handleClick = () => {
console.log('clicked');
};
return <Child onClick={handleClick} />;
}これ自体はパフォーマンス問題にはなりません。問題になるのは子コンポーネントが React.memo でラップされている場合です。onClick の参照が毎回変わるため、React.memo による最適化が無効になります。
React.memoとuseCallbackの組み合わせ
// React.memo でラップ:propsが変わらなければ再レンダリングしない
const Button = React.memo(function Button({
onClick,
label,
}: {
onClick: () => void;
label: string;
}) {
console.log(`Button "${label}" レンダリング`);
return <button onClick={onClick}>{label}</button>;
});
function Counter() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// ❌ useCallbackなし:textが変わるたびにhandleIncrementが再作成される
// → Button が再レンダリングされてしまう
const handleIncrement = () => setCount((c) => c + 1);
// ✅ useCallbackあり:依存配列が空なので常に同じ関数参照
// → textが変わってもButtonは再レンダリングされない
const handleIncrement = useCallback(() => {
setCount((c) => c + 1);
}, []); // setCount は参照が安定しているため依存不要
return (
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
<p>カウント: {count}</p>
<Button onClick={handleIncrement} label="+1" />
</div>
);
}useEffectの依存配列でuseCallbackを使う
コールバック関数を useEffect の依存配列に含めると、毎回新しい関数が作られて無限ループになります。
function DataFetcher({ onSuccess }: { onSuccess: (data: unknown) => void }) {
// ❌ onSuccess が毎回新しい関数 → useEffect が毎回実行される
useEffect(() => {
fetch('/api/data')
.then((r) => r.json())
.then(onSuccess);
}, [onSuccess]);
}
// 親コンポーネントで useCallback を使って安定させる
function Parent() {
const [result, setResult] = useState(null);
// ✅ 常に同じ関数参照を渡す
const handleSuccess = useCallback((data: unknown) => {
setResult(data);
}, []); // setResult は参照が安定しているため依存不要
return <DataFetcher onSuccess={handleSuccess} />;
}useCallbackを使わなくてよいケース
useCallback にもメモ化のコストがかかります。むやみに使うと逆効果です。
// ❌ 不要なuseCallback(子がReact.memoでラップされていない)
function Form() {
const handleSubmit = useCallback(() => {
// ...
}, []);
return <form onSubmit={handleSubmit}>...</form>; // 普通のフォーム
}使うべき目安
| 状況 | 判断 |
|---|---|
子が React.memo でラップされている |
使う |
useEffect の依存配列に渡す関数 |
使う |
| イベントハンドラを多数の子に渡す | 使う |
子が React.memo でラップされていない |
使わない |
| コンポーネント内部だけで使う関数 | 使わない |
実践:検索フォームの最適化
type SearchResult = { id: number; title: string };
const ResultItem = React.memo(function ResultItem({
result,
onSelect,
}: {
result: SearchResult;
onSelect: (id: number) => void;
}) {
return (
<li onClick={() => onSelect(result.id)}>{result.title}</li>
);
});
function SearchPage() {
const [query, setQuery] = useState('');
const [selected, setSelected] = useState<number | null>(null);
const [results, setResults] = useState<SearchResult[]>([]);
// selected が変わっても onSelect の参照は変わらない
const handleSelect = useCallback((id: number) => {
setSelected(id);
}, []); // setSelected は参照が安定
useEffect(() => {
if (!query) return;
fetch(`/api/search?q=${query}`)
.then((r) => r.json())
.then(setResults);
}, [query]);
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<ul>
{results.map((r) => (
<ResultItem key={r.id} result={r} onSelect={handleSelect} />
))}
</ul>
{selected && <p>選択: {selected}</p>}
</div>
);
}handleSelect を useCallback でメモ化したことで、query が変わって results が更新されても、onSelect の参照が変わらないため各 ResultItem の不要な再レンダリングを防げます。
たける
`React.memo` と `useCallback` をセットで使わないと意味がないってどういうことですか?
りこ
React.memoは「propsが変わらなければ再レンダリングしない」という最適化。でも関数を毎レンダリングで新しく作ると参照が変わって「propsが変わった」とみなされる。useCallbackで同じ関数参照を維持してはじめてReact.memoが効く。
なつみ
UIの実装だと、大きいリストの各行にコールバックを渡すときによく使う。スクロール中に各行が何度も再レンダリングされると体感の重さに出る。React.DevToolsのProfilerで確認してから入れる。
まとめ
useCallbackは関数参照をメモ化する(useMemoの関数版)- 子コンポーネントが
React.memoの場合や、useEffectの依存配列に渡す場合に有効 - 使いすぎはメモ化コストで逆効果になる
- セットアップの手間を省くには
React.memo+useCallbackをセットで考える
次の第10回では、Hookを組み合わせてロジックを再利用する カスタムHook を学びます。