useCallback — 関数のメモ化と子コンポーネント最適化
この章の目次開く
30秒解説 — useCallbackとは
結論から言うと、useCallback は 依存配列の値が変わらない限り、同じ関数を使い回す フックです。
使う理由は、関数の再生成を防ぎ、不要な再レンダーや useEffect の不要な再実行を避けるためです。
ただし、useCallback で包んだ関数の中身が速くなるわけではありません。変わるのは「毎回新しい関数として扱われるか」「前回と同じ関数として扱われるか」です。
useCallback は2つの引数を受け取ります。
const handleClick = useCallback(
() => {
doSomething();
}, // 第1引数: メモ化したい関数
[deps], // 第2引数: 関数を作り直す条件を並べた依存配列
);- 第1引数 は、メモ化したい関数そのものです。
- 第2引数 は、その関数を作り直す条件になる依存配列です。
依存配列の中身が前回と同じなら、useCallback は前回と同じ関数を返します。依存配列の中身が変わったら、新しい関数を返します。
学習者関数を毎回作るだけでそんなに問題になるの?
先生関数を作るコスト自体は小さいよ。問題になるのは、その関数を useEffect の依存配列に入れたり、メモ化された子コンポーネントに渡したりするときだね。
詳細解説 — 関数の参照を安定させるとは
コンポーネントが再レンダーされると、コンポーネント関数の中のコードはもう一度実行されます。 そのため、コンポーネント内で定義した関数も毎回新しく作られます。
function Parent() {
const [count, setCount] = useState(0);
const handleClick = () => {
console.log('clicked');
};
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>+1</button>
<button onClick={handleClick}>実行</button>
</div>
);
}count が変わると Parent は再レンダーされます。そのとき handleClick も新しい関数として作られます。
ここで大事なのは、関数を作り直すこと自体が悪いわけではないという点です。普通のボタンクリック処理なら、毎回新しい関数が作られてもほとんど問題になりません。
問題になるのは、その関数を渡した先や依存配列が「前回と同じ関数かどうか」を見ている場合です。
useCallback を使うと、依存配列が変わらない限り前回と同じ関数を返せます。
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>+1</button>
<button onClick={handleClick}>実行</button>
</div>
);
}この例では handleClick が何にも依存していないので、依存配列は空配列 [] です。count が変わって Parent が再レンダーされても、handleClick は前回と同じ関数として扱われます。
この「前回と同じ関数として扱われる」ことを活かす代表例が、React.memo された子コンポーネントに渡す場合と、useEffect の依存配列に含める場合です。
React.memoに関数を渡す場合
まず典型例として、React.memo でラップされた子コンポーネントにコールバックを渡すパターンがあります。
React.memoとは
React.memo は高階コンポーネント(Higher-Order Component)で、propsが前回と同じなら再レンダーをスキップします。
const ExpensiveChild = React.memo(function ExpensiveChild({ onClick }: { onClick: () => void }) {
// 重い描画処理...
return <button onClick={onClick}>実行</button>;
});React.memo はpropsの各値を Object.is(ほぼ ===)で比較します。文字列や数値なら値が同じならスキップされます。しかし関数オブジェクトは、中身が同じでも毎回新しいインスタンスが作られるため、=== 比較で常に false になります。
useCallbackなしの場合
function Parent() {
const [count, setCount] = useState(0);
// 毎回新しい関数オブジェクト → ExpensiveChildは毎回再レンダー
const handleClick = () => {
console.log('clicked');
};
return (
<div>
<p>{count}</p>
<button onClick={() => setCount((c) => c + 1)}>+1</button>
<ExpensiveChild onClick={handleClick} />
</div>
);
}count が変わるたびに Parent が再レンダーされ、新しい handleClick が生成されます。ExpensiveChild は React.memo で包まれていますが、onClick が毎回新しいので「propsが変わった」と判断し、毎回再レンダーされます。React.memo の意味がなくなっています。
useCallbackありの場合
function Parent() {
const [count, setCount] = useState(0);
// 依存する値がないので、常に同じ関数オブジェクトが返る
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount((c) => c + 1)}>+1</button>
<ExpensiveChild onClick={handleClick} />
</div>
);
}useCallback により handleClick の参照が安定するので、ExpensiveChild は「propsが同じ」と判断し、再レンダーをスキップできます。
useEffectの依存配列に関数を含める場合
もう一つの実務的な使いどころが、useEffect の中で関数を使っている場合です。
たとえば検索結果を取得するコンポーネントを考えます。
function SearchResults({ query }: { query: string }) {
const fetchResults = () => {
return fetch(`/api/search?q=${query}`);
};
useEffect(() => {
fetchResults().then(/* ... */);
}, []);
}このコードは一見動きそうですが、useEffect の中で fetchResults を使っているのに、依存配列に fetchResults が入っていません。
react-hooks/exhaustive-deps が有効な環境では、次のような警告が出ます。
React Hook useEffect has a missing dependency: 'fetchResults'. Either include it or remove the dependency array.日本語にすると、「useEffect の中で fetchResults を使っているので、依存配列に入れてください」という意味です。
では言われた通りに fetchResults を依存配列へ入れるとどうなるでしょうか。
function SearchResults({ query }: { query: string }) {
const fetchResults = () => {
return fetch(`/api/search?q=${query}`);
};
useEffect(() => {
fetchResults().then(/* ... */);
}, [fetchResults]);
}これで依存漏れはなくなりました。ただし、別の問題があります。
fetchResults はコンポーネント内で普通に定義している関数です。レンダーのたびに新しい関数として作られるので、依存配列に入れると毎レンダーで「変わった」と判定されやすくなります。その結果、useEffect が不要に再実行される原因になります。
このときは、次のような警告が出ることもあります。行番号はファイルによって変わります。
The 'fetchResults' function makes the dependencies of useEffect Hook (at line 10) change on every render. Move it inside the useEffect callback. Alternatively, wrap the definition of 'fetchResults' in its own useCallback() Hook.警告が言っていることは、かなり実用的です。
「その関数は毎レンダーで新しくなるので、依存配列が毎回変わります。useEffect の中に移動するか、useCallback で包んでください」という意味です。
エフェクト内でしか使わないなら中に入れる
まず考えるべきなのは、useCallback ではありません。
その関数を useEffect の中でしか使っていないなら、関数ごと useEffect の中に入れるのが一番シンプルです。
function SearchResults({ query }: { query: string }) {
useEffect(() => {
const fetchResults = () => {
return fetch(`/api/search?q=${query}`);
};
fetchResults().then(/* ... */);
}, [query]);
}これなら依存配列に必要なのは query だけです。fetchResults という外側の関数に依存しないので、関数の参照安定化を考える必要もありません。
外でも使うならuseCallbackで安定させる
一方で、同じ関数を useEffect の外でも使いたいことがあります。
たとえば、初回表示時にも検索し、ボタンを押したときにも同じ検索処理を使いたい場合です。
function SearchResults({ query }: { query: string }) {
const fetchResults = useCallback(() => {
return fetch(`/api/search?q=${query}`);
}, [query]);
useEffect(() => {
fetchResults().then(/* ... */);
}, [fetchResults]);
return <button onClick={() => fetchResults()}>再検索</button>;
}この場合、fetchResults は query が変わったときだけ新しい関数になります。
useEffect の依存配列にも安全に含められます。query が同じなら fetchResults の参照も同じなので、useEffect は不要に再実行されません。
ここでの useCallback の役割は、「ESLintの警告を黙らせること」ではありません。useEffect が実際に依存している関数を依存配列に入れつつ、その関数が毎レンダーで変わらないようにすることです。
useCallbackだけで速くなるわけではない
学習者とりあえず関数は全部 useCallback で包んでおけば速くなるんだよね?
先生それが落とし穴。useCallback
は受け取る側が参照の安定を活かせる時だけ効く。単体だとむしろ無駄なんだ。
重要なポイントとして、useCallback 単体ではパフォーマンス改善になりません。
function Parent() {
// useCallbackで関数をメモ化しているが...
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
// React.memoで包まれていない子 → 親が再レンダーされたら常に再レンダー
return <Child onClick={handleClick} />;
}Child が React.memo で包まれていない場合、親の再レンダーに伴って Child も常に再レンダーされます。handleClick の参照が安定していても意味がありません。
つまり useCallback は「参照が同じであることを、どこかが見ている」場合にだけ意味があります。
その代表例が、React.memo された子コンポーネント、useEffect の依存配列、useMemo の依存配列です。
依存配列にstateを含めるuseCallback
学習者useCallback
の依存配列にstateを入れたら、stateが変わるたびに関数が再生成されちゃう…。それってメモ化の意味なくない?
先生そこがポイント。updater function(setTodos(prev => ...) の形)を使えば、stateをクロージャで参照しなくて済むから依存配列から外せるよ。
useCallback の依存配列にstateを含める場合、そのstateが頻繁に変わると関数もそのたびに再生成されます。
function TodoApp() {
const [todos, setTodos] = useState<Todo[]>([]);
// todosが変わるたびに新しい関数が生成される
const handleDelete = useCallback(
(id: string) => {
setTodos(todos.filter((t) => t.id !== id));
},
[todos], // todosに依存
);
return <TodoList todos={todos} onDelete={handleDelete} />;
}これだとtodosが変わるたびに handleDelete も新しくなり、React.memo された TodoList のメモ化が効きにくくなります。
updater functionを使えば todos への依存を外せます。
const handleDelete = useCallback((id: string) => {
setTodos((prev) => prev.filter((t) => t.id !== id));
}, []); // 依存配列が空 → 常に同じ関数setTodos に関数を渡すことで、Reactが最新の todos を引数として渡してくれます。todos をクロージャ経由で参照する必要がなくなるので、依存配列から外せます。
これは「依存配列をズルして減らす」テクニックではありません。関数が本当に todos を直接参照しない形に変えているので、依存から外せるという話です。
useCallbackはuseMemoの特殊ケース
仕組みだけを見ると、useCallback は「関数を返す useMemo」と同じです。
// この2つは同じ意味
const handleClick = useCallback(() => {
console.log('clicked');
}, [deps]);
const handleClick = useMemo(
() => () => {
console.log('clicked');
},
[deps],
);ただ、実際のコードでは関数をメモ化したい意図がはっきり出るので、関数には useCallback を使う方が読みやすいです。
使うべきか迷ったときの判断基準
使う場面
useEffect の中でも外でも使う関数で、依存配列に安全に含めたい場合。
React.memo された子コンポーネントにコールバックpropsとして渡す関数。
useMemo やカスタムフックの依存配列に含める関数で、参照の安定化が必要な場合。
使わなくてよい場面
React.memo されていない子コンポーネントへのprops。メモ化しても子は結局再レンダーされます。
コンポーネント内で完結する関数(子に渡さない、依存配列にも入れない)。参照の安定性が誰にも利用されません。
JSXに直接書くインラインのイベントハンドラ。
// これにuseCallbackは不要
<button onClick={() => setCount((c) => c + 1)}>+1</button>まとめ
useCallback は、依存配列が変わらない限り同じ関数を返すフックです。
useEffect の不要な再実行を避けることです。
特に実務では、react-hooks/exhaustive-deps の警告に従って関数を依存配列へ入れたい場面があります。その関数が毎レンダーで作られるとエフェクトも毎回動きやすくなるため、useCallback で参照を安定させます。
ただし、エフェクト内でしか使わない関数はエフェクト内に移す方がシンプルです。useCallback を使うかどうかは、「その関数の参照が同じであることを、依存配列や React.memo が利用しているか」で判断してください。
