React の useMemo がメモ化されず毎回再計算される
useMemo は依存配列の値を Object.is で比較する。
オブジェクトや関数を依存に入れると参照が毎回変わり再計算される。
依存はプリミティブに絞り、参照は親側で useMemo / useCallback により安定化させる。
#react#usememo#memoization#dependency-array#hooks
公開:
要約
useMemo(() => expensive(x), [x]) がメモ化されないときは、ほぼ確実に依存配列の比較で参照不一致が起きている。
React は Object.is で比較するため、毎レンダリングで新しく作られるオブジェクトや関数を渡すと毎回再計算になる。
依存はプリミティブまで分解するのが基本方針。
よくある原因
- オブジェクト / 配列を依存に入れている:
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 中に直接計算するシンプルな書き方の方が、誤ったメモ化より破綻が少ない。