1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

useMemo 完全ガイド — 書く前に「本当に必要か」を測る

結論

デフォルトは「書かない」。 useMemo が必要なのは、次の3ケースだけ。

  1. 実測して重い計算(数千件のソート・フィルタ、パース処理など)
  2. 参照等価性が必要な場合(React.memo した子への props、他の hook の依存配列、Context の value)
  3. カスタム hook の戻り値を安定させたい場合(利用側で依存に入るため)

それ以外は、useMemo を書くこと自体がコスト。メモ化にも計算とメモリのコストがあり、多くのケースでは元の計算より高くつく。

さらに 2026 年時点では、React Compiler を導入すればこれらの手動メモ化のほとんどが不要になる。新規プロジェクトなら、まず Compiler の導入可否を検討するのが先。


1. useMemo が実際にやっていること

const value = useMemo(() => computeSomething(a, b), [a, b]);

やっていることは2つだけ。

  • 依存配列 [a, b] が前回と変わっていなければ、前回の計算結果をそのまま返す
  • 変わっていれば、関数を再実行して新しい結果を返す

依存の比較は Object.is による浅い比較。これは useEffect の依存配列と完全に同じルール(前回の useEffect 記事で触れたとおり)。

重要な注意: 保証ではなく最適化

React 公式ドキュメントは、**「useMemo はセマンティックな保証ではない」**と明記している。React は将来的に、メモリ解放のためにキャッシュを破棄する可能性がある。

// ❌ useMemo に「1回しか実行されないこと」を依存してはいけない
const id = useMemo(() => crypto.randomUUID(), []); // 再計算されうる

// ✅ 一意性が必要なら useState か useRef、あるいは useId
const [id] = useState(() => crypto.randomUUID());
const id2 = useId(); // DOM の id 用途ならこちら

useMemo は「正しさ」のために使わない。「速さ」のためだけに使う。


2. ケース①: 実測して重い計算

function ProductList({ products, keyword, sortKey }) {
  const visibleProducts = useMemo(() => {
    return products
      .filter((p) => p.name.includes(keyword))
      .sort((a, b) => a[sortKey] - b[sortKey]);
  }, [products, keyword, sortKey]);

  return <List items={visibleProducts} />;
}

これが正当化されるのは、products が数千件以上あり、かつ他の state 更新で頻繁に再レンダリングが起きる場合。100件程度なら、filter + sort は 1ms もかからない。useMemo のオーバーヘッドのほうが大きい。

測ってから判断する

const visibleProducts = useMemo(() => {
  console.time('filter+sort');
  const result = products.filter(...).sort(...);
  console.timeEnd('filter+sort');
  return result;
}, [products, keyword, sortKey]);

目安: 1ms 未満ならメモ化は不要。 実際にはブラウザの高速なマシンで測っているので、Performance タブの CPU throttling を 4x〜6x にして測ると実機に近くなる。

React DevTools の Profiler でコンポーネント単位のレンダリング時間を見るのも有効。「useMemo を外して遅くなるか」を確認してから入れる。


3. ケース②: 参照等価性を保つ

こちらのほうが実務では重要。計算が軽くても、参照が変わること自体が問題になる場面がある。

(a) 他の hook の依存配列に入る場合

// ❌ options は毎レンダリング新しいオブジェクト → effect が毎回再実行
function Chart({ width, height, data }) {
  const options = { width, height, responsive: true };

  useEffect(() => {
    const chart = new ChartLib(ref.current, options);
    return () => chart.destroy();
  }, [options]); // 実質「毎レンダリング」
}
// ✅ 参照を安定させる
const options = useMemo(
  () => ({ width, height, responsive: true }),
  [width, height]
);

ただし前回の useEffect 記事で書いたとおり、この場合はオブジェクト生成を effect の中に移動するのが第一選択。useMemo は、そのオブジェクトを複数箇所で使い回す必要があるときの手段。

(b) React.memo した子コンポーネントの props

const ExpensiveList = React.memo(function ExpensiveList({ items }) {
  // 重いレンダリング
});

function Parent({ allItems }) {
  const [count, setCount] = useState(0);

  // ❌ count が変わるたびに新しい配列 → React.memo が無意味になる
  const items = allItems.filter((i) => i.visible);

  // ✅ 参照が保たれるので、count が変わっても ExpensiveList は再レンダリングされない
  const items2 = useMemo(() => allItems.filter((i) => i.visible), [allItems]);

  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>
      <ExpensiveList items={items2} />
    </>
  );
}

React.memouseMemo はセットで使わないと効果がない。 子を memo 化しても、props に毎回新しいオブジェクト・配列・関数を渡していれば素通りする。逆に、子を memo 化していないなら親側の useMemo も無駄。

(c) Context の value

これは事故率が高い。

// ❌ Provider が再レンダリングされるたびに、全 consumer が再レンダリング
function AuthProvider({ children }) {
  const [user, setUser] = useState(null);

  return (
    <AuthContext.Provider value={{ user, setUser }}>
      {children}
    </AuthContext.Provider>
  );
}
// ✅ value をメモ化する
function AuthProvider({ children }) {
  const [user, setUser] = useState(null);

  const value = useMemo(() => ({ user, setUser }), [user]);
  // setUser は React が参照を保証するので依存に入れても実質不変

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

Redux 記事で触れた「state と dispatch の Context を分ける」パターンも、根っこは同じ問題への対処。


4. useCallback との関係

useCallback は useMemo の関数版。この2つは等価:

const fn = useCallback((x) => doSomething(x, a), [a]);

// ↑ は ↓ の糖衣構文
const fn = useMemo(() => (x) => doSomething(x, a), [a]);

判断基準も同じ。React.memo した子に渡す、または他の hook の依存に入る関数だけ useCallback する。

// ❌ 意味がない — 子が memo 化されていないなら参照が安定しても無駄
const handleClick = useCallback(() => setOpen(true), []);
return <button onClick={handleClick}>開く</button>; // 素の DOM 要素

DOM 要素の onClick に渡すだけなら useCallback は完全に無駄。React は関数の参照が変わってもリスナーを張り直さない。


5. アンチパターン

❌ プリミティブをメモ化する

// 無意味 — number に参照等価性の問題は存在しない
const total = useMemo(() => a + b, [a, b]);
const isValid = useMemo(() => name.length > 0, [name]);

useMemo のオーバーヘッド(依存配列の生成・比較・クロージャ保持)のほうが a + b より高い。

❌ 「念のため」全部に付ける

// レビューで見かける最悪パターン
const a = useMemo(() => props.x, [props.x]);
const b = useMemo(() => props.items.length, [props.items]);
const c = useCallback(() => {}, []);

可読性が落ち、依存配列の管理漏れによるバグ(古い値を掴んだままになる)を生む。メモ化はバグを増やす方向に働くことを忘れない。

❌ 依存配列にオブジェクトを直接入れる

// ❌ config が毎回新しければ、useMemo は毎回再計算 = 意味がない
const result = useMemo(() => compute(config), [config]);

// ✅ プリミティブに分解する
const result = useMemo(() => compute({ a, b }), [a, b]);

依存配列にはプリミティブだけを置く。 これも useEffect と共通の原則。

❌ 条件分岐やループの中で呼ぶ

// ❌ Hooks のルール違反
if (isReady) {
  const value = useMemo(() => compute(), []);
}

6. React Compiler — 手動メモ化が要らなくなる

React Compiler は、ビルド時にコンポーネントを解析して自動でメモ化を挿入するツール。導入すると、上記の useMemo / useCallback / React.memo のほとんどが不要になる。

// Compiler 導入後は、これで十分
function ProductList({ products, keyword }) {
  const visible = products.filter((p) => p.name.includes(keyword));
  return <List items={visible} />;
}

導入の前提条件:

  • React のルールを守っていること(レンダリング中のミューテーション禁止、props の書き換え禁止など)
  • eslint-plugin-react-hooks の警告がクリーンであること

既存プロジェクトでは、まず ESLint ルールを通してから段階的に有効化するのが定石。Compiler が「安全に最適化できない」と判断したコンポーネントはスキップされるので、壊れるより「効かない」形で失敗する設計になっている。

導入状況やオプションはバージョンで変わるため、採用前に公式ドキュメントで最新の推奨構成を確認したほうがいい。


7. 判断フロー

その値、参照等価性が必要か?
├─ Yes(React.memo の props / hook の依存 / Context value)
│   └─ useMemo する
└─ No
    └─ 計算が重いか?(実測して 1ms 以上)
        ├─ Yes → useMemo する
        └─ No  → 書かない

そして React Compiler を導入しているなら、上記フローに入る前に「Compiler に任せられないか」を先に考える


8. メモ化より先にやるべきこと

useMemo に手を出す前に、より効果が大きい対策がある。

state を下に移す

// ❌ 入力のたびに重い ExpensiveTree まで再レンダリング
function Page() {
  const [text, setText] = useState('');
  return (
    <>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <ExpensiveTree />
    </>
  );
}

// ✅ state を使う範囲に閉じ込める
function SearchInput() {
  const [text, setText] = useState('');
  return <input value={text} onChange={(e) => setText(e.target.value)} />;
}

function Page() {
  return (
    <>
      <SearchInput />
      <ExpensiveTree />
    </>
  );
}

メモ化ゼロで再レンダリングが消える。 こちらのほうが常に優れている。

children として渡す

// 親の state が変わっても、children の参照は変わらないので再レンダリングされない
function Layout({ children }) {
  const [open, setOpen] = useState(false);
  return <div className={open ? 'open' : ''}>{children}</div>;
}

<Layout>
  <ExpensiveTree />
</Layout>

そもそもリストが巨大なら仮想化

数千行のテーブルを高速化したいなら、useMemo ではなく @tanstack/react-virtual などで画面内の行しか描画しないのが本質的な解決。


まとめ

  • useMemo は「速さ」のための道具。「正しさ」を依存させない
  • デフォルトは書かない。測ってから入れる
  • 必要なのは、重い計算 / 参照等価性 / カスタム hook の戻り値の3ケース
  • React.memo とセットでないと参照の安定化は意味を持たない
  • Context の value のメモ化漏れは事故率が高い
  • 依存配列にはプリミティブだけを置く(useEffect と同じ原則)
  • React Compiler を導入できるなら、手動メモ化の大半は不要になる
  • メモ化より先に、state を下に移す・children で渡す・仮想化する
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?