TOOLS BOX/ガイド/React の再レンダリング最小化パターン
Concept

React の再レンダリング最小化パターン

React の再レンダリングを必要な範囲に絞るためのパターン。memo / useMemo / useCallback の役割の違いと使いどころ、context と組み合わせるときの注意点、過剰最適化を避けるための考え方を整理する。

reactmemouseMemouseCallbackre-renderperformancememoizationcontext

どういう場面で使うか

  • ·memo: 子コンポーネントが親の再レンダリングで不必要に再描画されていると計測で確認できたとき
  • ·useMemo: コンポーネントをまたいで渡すオブジェクト・配列の参照を安定させたいとき、または重い計算を毎レンダリング実行したくないとき
  • ·useCallback: memo でラップした子に関数を props として渡すとき、または useEffect の依存配列に関数を含めるとき

注意点 / Pitfalls

  • ·memo を付けても、props にオブジェクト・配列・関数リテラルを渡すと毎回新しい参照になり効果がない
  • ·useMemo / useCallback は計算コストがある。闇雲に使うと最適化ではなくオーバーヘッドになる
  • ·useCallback だけ使っても、memo でラップしていない子には再レンダリング防止効果がない
  • ·context の value に毎レンダリング新しいオブジェクトを渡すと、useContext を使う全子が再レンダリングされる
  • ·計測せずに最適化しても、体感できる改善がないことが多い。React DevTools Profiler で問題箇所を特定してから適用する

補足

再レンダリング最小化は「計測して問題が見えてから対処する」が基本。構造の問題(state の置き場所・context の分け方)を先に直すと、memo なしで解決することも多い。最適化は最後の手段ではなく、設計が正しくない場合の補助手段として位置づける。

再レンダリングが起きる仕組み

React は以下の条件でコンポーネントを再レンダリングします。

1. 自身の state が変わった
2. 受け取った props が変わった(参照の比較)
3. 親コンポーネントが再レンダリングされた(props が変わらなくても)
4. useContext で参照している context の value が変わった

3 が問題になるケースが多く、「親が変わっただけなのに重い子が毎回再描画される」という状況が起きます。

3 つのツールの役割

ツール何をメモ化するか目的
memoコンポーネント自体親の再レンダリングが props 変化なしに伝播するのを止める
useMemo計算結果・オブジェクト・配列重い計算の再実行防止 / 参照の安定化
useCallback関数関数参照を安定させて memo の効果を活かす

memo — 親の再レンダリング伝播を止める

// ❌ memo なし: Parent が再レンダリングされるたびに ExpensiveList も再レンダリングされる
function Parent() {
  const [count, setCount] = useState(0);
  return (
    <>
      <button onClick={() => setCount((c) => c + 1)}>{count}</button>
      <ExpensiveList items={staticItems} /> {/* count が変わるたびに再実行される */}
    </>
  );
}

// ✅ memo: props が変わらない限り再レンダリングをスキップする
const ExpensiveList = memo(function ExpensiveList({ items }: { items: Item[] }) {
  return <ul>{items.map((i) => <li key={i.id}>{i.name}</li>)}</ul>;
});

memo が効かないパターン:

// ❌ props にオブジェクトリテラルを渡す → 毎レンダリングで新しい参照 → memo が素通りする
<ExpensiveList config={{ sortBy: "name" }} />

// ❌ props に関数リテラルを渡す → 毎レンダリングで新しい参照 → memo が素通りする
<ExpensiveList onSelect={() => setSelected(id)} />

// ✅ useMemo / useCallback で参照を安定させる(後述)

useMemo — 参照の安定化と重い計算

useMemo は 2 つの目的で使います。

① 重い計算を毎レンダリング実行しない

function ProductList({ products }: { products: Product[] }) {
  // ❌ products が変わっていなくても毎レンダリングで計算される
  const sorted = products.slice().sort((a, b) => b.price - a.price);

  // ✅ products が変わったときだけ計算する
  const sorted = useMemo(
    () => products.slice().sort((a, b) => b.price - a.price),
    [products]
  );
  return <ul>{sorted.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}

② memo に渡すオブジェクト・配列の参照を安定させる

// ❌ config が毎レンダリングで新しいオブジェクトになる → memo が効かない
function Parent() {
  const [count, setCount] = useState(0);
  const config = { sortBy: "name", order: "asc" }; // 毎回新しい参照

  return <MemoizedChild config={config} />;
}

// ✅ useMemo で参照を固定する
function Parent() {
  const [count, setCount] = useState(0);
  const config = useMemo(() => ({ sortBy: "name", order: "asc" }), []); // 参照が変わらない

  return <MemoizedChild config={config} />;
}

useCallback — 関数参照の安定化

// ❌ onSelect が毎レンダリングで新しい関数になる → MemoizedChild の memo が効かない
function Parent({ items }: { items: Item[] }) {
  const [selected, setSelected] = useState<string | null>(null);

  return (
    <MemoizedChild
      items={items}
      onSelect={(id) => setSelected(id)} // 毎回新しい関数
    />
  );
}

// ✅ useCallback で関数参照を安定させる
function Parent({ items }: { items: Item[] }) {
  const [selected, setSelected] = useState<string | null>(null);
  const handleSelect = useCallback((id: string) => setSelected(id), []); // 参照が変わらない

  return <MemoizedChild items={items} onSelect={handleSelect} />;
}

useCallback 単体では再レンダリングを防げない:

useCallback だけ使っても、受け取る側が memo でラップされていなければ
親の再レンダリングはそのまま子に伝播する。

useCallback が効果を発揮するのは:
  ① memo でラップした子に関数を渡すとき
  ② useEffect の依存配列に関数を含めるとき(無限ループ防止)

context と組み合わせるときの注意点

context の value に毎レンダリングで新しいオブジェクトを渡すと、useContext を使うすべての子が再レンダリングされます。

// ❌ value に毎回新しいオブジェクトを渡す
function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<"light" | "dark">("light");

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}> {/* 毎回新しいオブジェクト */}
      {children}
    </ThemeContext.Provider>
  );
}

// ✅ useMemo で value を安定させる
function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<"light" | "dark">("light");
  const value = useMemo(() => ({ theme, setTheme }), [theme]); // theme が変わったときだけ更新

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

変更頻度で context を分割する:

// ❌ 変わる値と変わらない値を 1 つの context に混ぜる
const AppContext = createContext({ user: null, theme: "light", cartCount: 0 });
// → cartCount が変わるたびに user・theme を参照しているコンポーネントも再レンダリングされる

// ✅ 変更頻度が異なる値を context で分ける
const UserContext = createContext(null);       // ほぼ変わらない
const ThemeContext = createContext("light");   // たまに変わる
// cartCount → Zustand store へ(頻繁に変わる)

過剰最適化のサイン

以下は最適化の必要がないことが多い:

□ 軽い計算(配列の map / filter / 数値計算)に useMemo を付ける
  → メモ化のコスト(依存配列の比較)の方が大きいことがある

□ すべてのコンポーネントに memo を付ける
  → props が毎回変わるコンポーネントは memo があっても毎回実行される

□ 問題が起きていない箇所を事前に最適化する
  → React DevTools Profiler で計測してから判断する

□ memo を付けずに useCallback だけ使う
  → 子が memo でラップされていなければ効果がない

判断フロー

子コンポーネントが不必要に再レンダリングされている
  ↓
① まず構造を見直す(state の置き場所・context の分割)
  → 構造を直すだけで解決することが多い

② それでも重い場合 → React DevTools Profiler で計測

③ 子コンポーネントを memo でラップする

④ memo に渡している props を確認する
  ・オブジェクト・配列 → useMemo で参照を安定させる
  ・関数 → useCallback で参照を安定させる

よくある誤用まとめ

誤用正しい判断
全コンポーネントに memo計測して問題のある箇所だけに絞る
memo なしで useCallback だけ使う子が memo でラップされていないと効果なし
軽い計算に useMemoメモ化コストの方が大きい可能性がある
context value にオブジェクトリテラルuseMemo で参照を安定させる
最適化を先に考えるまず構造(state の置き場所・責務分離)を正す

データ受け渡しの設計判断 → React Hook によるデータ受け渡しパターン

グローバル store と private state の使い分け → グローバル store と private state の使い分け

Zustand selector による再レンダリング制御 → zustand-selector-optimization

関連ドキュメント

関連サンプル

同じテーマや技術スタックを使った実装例