useMemo(() => 重い計算()) と書いて依存配列を省略した。どうなる?
解説
useMemoの第2引数である依存配列を省略すると、毎回のレンダリングで関数が再実行されます。つまりメモ化が一切機能せず、useMemoで囲んでいないのと同じ状態です。「useMemoを書いたから最適化されているはず」と思い込んでいると、パフォーマンス上の問題に気づけない厄介なパターンです。選択肢Aの「初回だけ計算される」は、依存配列を空配列 [] にした場合の動作です。空配列は「依存する値がない=変わる理由がない」という意味なので、初回の結果がずっと使い回されます。「省略」と「空配列」はまったく別の挙動になる点が重要です。選択肢Cのコンパイルエラーにはなりません。依存配列はTypeScript上も必須引数ではなく、省略しても型エラーは出ません。ESLintのreact-hooks/exhaustive-depsルールを有効にしていれば警告は出ますが、それはあくまでlintの警告であってコンパイルエラーではありません。選択肢Dのようにundefinedが返ることもなく、関数の戻り値がそのまま返されます。依存配列の3パターンを整理するuseMemoの依存配列には3つの書き方があり、それぞれ動作が異なります。useMemo(fn, [a, b]) — aまたはbが変わったときだけ再計算する。最も一般的な使い方useMemo(fn, []) — 初回マウント時に1回だけ計算し、以降はキャッシュを返し続けるuseMemo(fn) — 依存配列そのものを省略。毎レンダリングで再計算され、メモ化の意味がなくなるこの違いはuseEffectやuseCallbackでも同じルールです。依存配列の省略と空配列は、React Hooksを使う上で最初に押さえるべき区別といえます。そもそもuseMemoはいつ使うべきかuseMemoは「計算結果のキャッシュ」を目的としたフックです。レンダリングのたびに実行すると重い処理——たとえば数千件の配列をフィルタリング・ソートするような場面——で効果を発揮します。逆に、a + bのような単純な計算にまでuseMemoを使うと、依存配列の比較処理というオーバーヘッドが加わるぶん、かえって遅くなることがあります。React公式ドキュメントでも「実際にパフォーマンス上の問題が計測できた箇所に絞って使う」ことが推奨されています。eslintルールで省略ミスを防ぐ依存配列の書き忘れや、必要な値の入れ忘れは手動で見つけるのが困難です。eslint-plugin-react-hooksのexhaustive-depsルールを有効にしておくと、依存配列の過不足をエディタ上で即座に警告してくれます。Create React AppやNext.jsのデフォルト設定ではこのルールが最初から有効になっているため、警告が出たら無視せず対応する習慣をつけておくと、今回のような落とし穴を踏まずに済みます。