次のReactコンポーネントで、useCallbackを使う意味が最もあるのはどのケースですか?
解説
正解はBです。useCallbackの本質は「関数の参照を再レンダリング間で同じに保つこと」であり、その参照の同一性が意味を持つ場面でしか効果がありません。React.memoでメモ化された子は、propsを前回と浅く比較して変化が無ければ再レンダリングをスキップしますが、関数リテラルは毎レンダリング新しいインスタンスとして生成されるため、useCallbackで包まないと毎回「propsが変わった」と判定されてmemoが無効化されます。Aの素のDOM要素のonClickは参照が変わっても再レンダリングコストに影響しないので無意味、Cの呼び出し1回のヘルパーはそもそもメモ化する対象ではありません。Dについては、useStateが返すセッター関数はReactが参照の安定性を保証してくれているので、わざわざ包み直すのはむしろ無駄です。useCallbackは「関数を速くする」フックではない初学者がよく誤解するのですが、useCallbackは関数の実行を速くするわけではありません。やっていることは「依存配列が変わらない限り、前回作った関数オブジェクトを使い回す」だけです。つまり恩恵があるのは、その関数オブジェクトの参照の同一性が何かの判定に使われるときだけ。具体的には次の2パターンに集約されます。React.memoされた子コンポーネントへのpropsとして渡す関数useEffectやuseMemoの依存配列に含める関数これ以外の場所で使っても、メモ化のための比較コストが増えるだけで体感できる効果はありません。「とりあえず全部useCallbackで包む」がアンチパターンと言われる所以です。React.memoとセットで初めて意味が出るBが正解になる理由をもう少し具体的に見てみます。const Child = React.memo(({ onClick }) => { console.log('Child rendered') return <button onClick={onClick}>click</button> }) function Parent() { const [count, setCount] = useState(0) // useCallbackで包まないと、countが変わるたびに // handleClickの参照が変わり、ChildのReact.memoが効かない const handleClick = useCallback(() => { console.log('clicked') }, []) return ( <> <button onClick={() => setCount(c => c + 1)}>{count}</button> <Child onClick={handleClick} /> </> ) }useCallbackを外すと、Parentが再レンダリングされるたびにChildも再レンダリングされ、React.memoの意味が消えます。逆に言えば、子がReact.memoで包まれていない普通のコンポーネントなら、useCallbackを付けても何も変わりません。useStateのセッターは元から安定しているDが不正解な理由も押さえておきましょう。useStateが返すsetCountやuseRefが返すref.currentなど、React自身が「同じ参照を返す」と保証しているAPIがいくつかあります。これらは依存配列に入れる必要すらなく、useCallbackで包み直すのは完全に無駄です。「すでに安定しているものをさらに安定させようとしない」という感覚は、メモ化系フックを使うときの大事な指針になります。なお将来的にはReact Compilerがこの種の最適化を自動でやってくれるため、useCallbackを手書きする機会は徐々に減っていく見込みです。