React の useMemo がメモ化されず毎回再計算される
useMemo は依存配列の値を Object.is で比較する。
オブジェクトや関数を依存に入れると参照が毎回変わり再計算される。
依存はプリミティブに絞り、参照は親側で useMemo / useCallback により安定化させる。
公開: 更新:
要約
useMemo(() => expensive(x), [x]) がメモ化されないときは、ほぼ確実に依存配列の比較で参照不一致が起きている。
React は Object.is で比較するため、毎レンダリングで新しく作られるオブジェクトや関数を渡すと毎回再計算になる。
依存はプリミティブまで分解するのが基本方針。
実行例
同じ id=1 のまま 3 回レンダーすると、毎回作り直すオブジェクトを依存に入れた版も依存配列を渡し忘れた版も計算が 3 回走り、中身が同じ { id: 1 } どうしの Object.is は false を返した。
依存を id そのものに分解した版では計算は 1 回にとどまり、id を 2 に変えたときだけ 2 回目が走っている。
$ node bad.js
id=1 のまま 3 回レンダーしたときの計算回数: 3
参照の比較: false$ node bad_no_deps.js
依存配列なしのときの計算回数: 3$ node solve_primitive.js
id=1 のまま 3 回レンダーしたときの計算回数: 1
id=2 に変えた後の計算回数: 2
$ echo $?
0— 2026-09-17 時点の出力
検証環境
- 検証日
- 実行環境
node:20Debian GNU/Linux 12 (bookworm)- バージョン
- Node.js 20.20.2
- npm 10.8.2
- Python 3.11.2
- Git 2.39.5
この記事の「実行例」は、上記の環境で実際にコマンドを実行して得られた出力をそのまま掲載しています。 再現手順はリポジトリの検証スクリプトとして管理し、定期的に再実行して出力を更新しています。
よくある原因
- オブジェクト / 配列を依存に入れている:
useMemo(() => f(opts), [opts])でoptsが毎回新しいオブジェクトなら、メモ化は実質効かない。 - props 由来の参照不安定: 親が
<Child config={{ a: 1 }} />と書くと、子から見てconfigは毎回新規参照になる。 - 依存配列を省略している:
useMemo(() => calc())のように 2 引数目を渡し忘れると、毎レンダリングで再計算される。 - キャッシュは破棄され得る: React は内部的にキャッシュを破棄することがあり、安定参照を必ず保証する用途には
useMemo単体では不十分な場合がある。
解決策
1. 依存をプリミティブに分解
// NG: opts は毎回新規参照
const result = useMemo(() => expensive(opts), [opts]);
// OK: id / size のプリミティブで判定
const result = useMemo(() => expensive({ id, size }), [id, size]);公式の useMemo リファレンス(新しいタブで開く) のとおり、依存は Object.is で比較される。
2. 親側で参照を固める
// 親
const config = useMemo(() => ({ a, b }), [a, b]);
return <Child config={config} />;// 子
const view = useMemo(() => render(config), [config]);子で何度 useMemo を書いても、親から毎回新しい参照が流れてくれば無意味。
修正は親側で行う。
3. 依存配列を省略しない
// NG: 2 引数目を渡し忘れ
const x = useMemo(() => compute());
// OK
const x = useMemo(() => compute(), [a, b]);ESLint の react-hooks/exhaustive-deps を有効にすると、依存抜けや省略を機械的に検出できる。
4. メモ化が本当に必要か再考する
useMemo 自体にコストがあるため、計算が軽いなら付けない方が速いことが多い。You Might Not Need an Effect 公式(新しいタブで開く) で扱われている通り、render 中に直接計算するシンプルな書き方の方が、誤ったメモ化より破綻が少ない。