再レンダリングが起きる仕組み
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