フロントエンドの世界へようこそ。Reactの実務にどっぷり浸かっていると、「なんとなく`useCallback`で囲んでおけばパフォーマンスが良くなるはず」という、いわゆる“お守り”的な運用に陥ってしまうことがよくある。
だが、厳しい現実を言わせてもらえば、理解せずにとりあえず使う`useCallback`ほど、メモリを無駄に食い、コードを汚し、デバッグを困難にするものはない。
今日は、中級者の壁を越えるために、「なぜ・いつ・どう使うべきか」という本質を叩き込んでいこう。
—
1. なぜ「関数」が再レンダリングのトリガーになるのか?
Reactのコンポーネントが再レンダリングされるとき、関数コンポーネントの中身は上から下まで全部再実行される。これが基本原則だ。
ここで重要なのが「関数の参照」だ。
const handleClick = () => { console.log(‘clicked’); };
このコードがコンポーネント内にあると、レンダリングのたびに、JavaScriptエンジンは「新しいメモリ領域」に関数を生成し直す。つまり、前回の`handleClick`と今回の`handleClick`は、中身が同じでも、参照先(メモリアドレス)が異なる別物とみなされる。
React.memoでラップした子コンポーネントにこの関数をpropsとして渡すと、どうなるか?
「propsが変わった」と判定され、子コンポーネントは容赦なく再レンダリングされる。これが、パフォーマンス最適化の努力を水泡に帰す典型的なパターンだ。
2. useCallbackの真価:参照の安定化
`useCallback`は、その関数をメモリ上に「固定」し、依存配列(Dependency Array)が変わらない限り、同じ参照先を使い回すようにReactに命じるフックだ。
実践的なサンプルコード
以下のコードは、リストアイテムをレンダリングする際の定番パターンだ。
import React, { useState, useCallback } from ‘react’;
// React.memoでラップすることで、propsが変わらない限り再レンダリングを阻止する
const ListItem = React.memo(({ item, onClick }) => {
console.log(`ListItem ${item.id} がレンダリングされました`);
return
;
});
const ListContainer = () => {
const [items] = useState([
{ id: 1, name: ‘React’ },
{ id: 2, name: ‘Next.js’ },
]);
// useCallbackで関数をメモ化。
// 依存配列が空なので、この関数はコンポーネントのライフサイクルを通して唯一の参照を維持する。
const handleClick = useCallback((id) => {
console.log(‘ID:’, id);
}, []); // 依存関係がないため、再生成されない
return (
-
{items.map((item) => (
))}
);
};
3. 現場で陥る「依存配列」の罠
`useCallback`で最も多いミスが、依存配列の管理不足だ。
// 悪い例
const handleSave = useCallback(() => {
console.log(data); // dataを参照しているのに配列に含まれていない
}, []); // 警告が出るし、古いdataを参照し続ける(クロージャの罠)
これを解決するために、とりあえず依存配列に全て突っ込むのは考えものだ。もし`data`が変わるたびに`handleSave`が再生成されるなら、結局`ListItem`も再レンダリングされることになり、`useCallback`を使う意味が消滅する。
シニアエンジニアの解決策:関数型更新
依存配列を減らすための魔法が「関数型更新」だ。
// 良い例:状態に依存せず、常に最新の値を取得する
const handleSave = useCallback(() => {
setData(prevData => […prevData, newItem]);
}, [newItem]); // newItemが変わったときだけ再生成される
`setData`に関数を渡すことで、stateの更新時にその時点の最新のstateを取得できる。これにより、`data`自体を依存配列に入れる必要がなくなる。これはパフォーマンス最適化の定石だ。
4. 最後に:賢いエンジニアは「使わない」選択もする
ここまで`useCallback`の重要性を説いてきたが、最後に一つだけ忠告がある。
「すべての関数を`useCallback`で囲むな」
`useCallback`を使うには、React内部で依存配列を比較するオーバーヘッドが発生する。単純なボタンのハンドラや、再レンダリングコストが無視できる小さなコンポーネントに乱用すると、かえって処理が重くなることさえある。
- `React.memo`を使っている子コンポーネントに渡す関数
- 重い計算や複雑な子コンポーネントのレンダリングをトリガーする関数
これらに絞って使うのが、プロの判断だ。「なんとなく」ではなく、「この関数が再生成されると、あそこの再レンダリングを誘発してしまう」というレンダリングの連鎖が見えている状態で初めて使うこと。
それができるようになったとき、君はもう一段階上のReactエンジニアになっているはずだ。現場のコードで迷ったら、まずはレンダリングの因果関係を紙に書き出してみろ。それが一番の近道だぞ。

コメント