useState / useMemo / useCallback / React.memo / クロージャー の理解を深める
このメモのゴール
- 「なぜ再レンダリングが起こるのか」
- 「
useState/useMemo/useCallback/React.memoがそれぞれ何をしているのか」 - 「クロージャ と 参照の安定 が、React の最適化にどう関係するのか」
- 「いつ
useCallbackが本当に必要で、いついらないのか」
を、自分で説明できるレベルで整理する。
1. React の再レンダリングの基本
再レンダリングが起こるきっかけ
const [state, setState] = useState(initial);
-
setState(...)を呼ぶと、コンポーネントの再レンダリングが予約される。 - その後、React がコンポーネント関数を もう一度最初から実行 する。
流れイメージ:
-
setState(newValue)を呼ぶ - React がそのコンポーネントの「再レンダー」をスケジュール
- 再レンダー中に
-
useStateが新しい値を返す -
useCallback/useMemoが依存配列をチェック - JSX を return
-
- React が差分を計算して DOM を更新
ポイント
-
setStateを呼んだ瞬間にuseMemo/useCallbackが動くのではなく、次の再レンダーの中で評価される。
2. プリミティブ vs 参照型 と ===
React.memo や Hook の依存配列は すべて === で比較される。
プリミティブ(値そのもの)
const A = "perfect";
const B = "perfect";
A === B; // true
-
string,number,booleanなどは 値が同じなら===も同じ。
参照型(オブジェクト / 配列 / 関数)
const obj1 = { a: 1 };
const obj2 = { a: 1 };
obj1 === obj2; // false(中身が同じでも別オブジェクト)
const fn1 = () => {};
const fn2 = () => {};
fn1 === fn2; // false(コードが同じでも、別々に作れば別関数)
const fn = () => {};
const A = fn;
const B = fn;
A === B; // true(同じ関数オブジェクトを指している)
React は「変数名」ではなく「中に入っている値(参照)」の === 結果だけを見ている。
3. クロージャとは何か(React に直結する重要ポイント)
一言でいうと
「関数 + その関数が“外側から捕まえた変数の環境”」
例:
function createCounter() {
let count = 0;
return function () {
count++;
console.log(count);
};
}
const counter1 = createCounter();
const counter2 = createCounter();
counter1(); // 1
counter1(); // 2
counter2(); // 1
counter1(); // 3
-
counter1はcount=0からスタートした「世界」を持っていて、それを閉じ込めている。 -
counter2は別のcount=0を持つ「別の世界」。 - 関数が、自分が作られた時点のスコープ(変数)を覚え続けている状態 = クロージャ。
React コンポーネントとクロージャ
function MyComponent() {
const [keyword, setKeyword] = useState("");
const logKeyword = () => {
console.log(keyword);
};
return <button onClick={logKeyword}>Log</button>;
}
- レンダー1回目:
logKeyword1 = () => console.log(keyword1); - レンダー2回目:
logKeyword2 = () => console.log(keyword2);
見た目同じコードでも、JavaScriptエンジン的には
「違う keyword を閉じ込めた別のクロージャ(関数)」。
logKeyword1 === logKeyword2; // false
→ これが「毎回別の関数が作られている」正体。
4. React.memo がやっていること
const Child = React.memo(function Child(props) {
console.log("Child render");
return <div>{props.value}</div>;
});
React.memo は 毎回レンダリング前に こう判定しているだけ:
if (
prevProps.value === nextProps.value &&
prevProps.onClick === nextProps.onClick &&
...
) {
// すべて === なら
// → 子コンポーネントの render をスキップ
} else {
// どれかが違えば
// → 子コンポーネントの render を実行
}
-
React.memoは 「前回 vs 今回の props」だけを見る。 - 関数の中身も useState も知らない。
- プリミティブは値が同じなら OK。
- 関数/配列は 同じオブジェクト(参照)じゃないと NG。
5. useMemo と React.memo の違い
React.memo
「コンポーネントまるごとレンダリングをスキップする仕組み」
-
親が再レンダーしても、
props が
===で同じなら、子コンポーネントの関数自体を呼ばない。
useMemo
const filtered = useMemo(() => {
return list.filter(fn); // heavy
}, [list, fn]);
「**レンダリングはする前提で、その中の“重い計算だけ”をスキップする仕組み」
- 子がレンダーされたときだけ評価される。
- 依存が変わっていなければ 前回の計算結果をそのまま返す。
- 依存が変わっていれば計算し直す。
まとめ
-
React.memo→ 「子コンポーネントを呼ばない」ことができる -
useMemo→ 「子は呼ぶけど、中の heavy 処理だけ止める」ことができる
6. useCallback が必要になる理由(クロージャ × React.memo × useMemo)
問題パターン:関数 props が毎回違う
function Parent() {
const [list] = useState(bigList);
const [unrelated, setUnrelated] = useState(0);
// 見た目は毎回同じだが、毎レンダーで新しい関数(別クロージャ)が作られる
const filterFn = (item) => item.includes("foo");
return (
<>
<button onClick={() => setUnrelated(n => n + 1)}>+1</button>
<Child list={list} filterFn={filterFn} />
</>
);
}
const Child = React.memo(function Child({ list, filterFn }) {
const filtered = useMemo(
() => list.filter(filterFn), // heavy(1万件フィルタとか)
[list, filterFn]
);
return <ListView items={filtered} />;
});
このとき:
- 親が
unrelatedのせいで再レンダーされるたびに、-
filterFnは毎回「新しい関数」(CDパターン)
-
- React.memo から見ると:
prevProps.filterFn === nextProps.filterFn // false
→ 子コンポーネントは 毎回レンダーされる。
- 子レンダー中の
useMemoでも:
deps [list, filterFn] が前回と違う
→ 毎回 heavy な filter が走る
結果 → 「条件も list も変わっていないのに、毎回1万件フィルターされる」地獄。
解決:useCallback で「同じ関数」として扱わせる
function Parent() {
const [list] = useState(bigList);
const [unrelated, setUnrelated] = useState(0);
const filterFn = useCallback(
(item) => item.includes("foo"),
[] // 依存: 条件が変わらない限り常に同じ関数オブジェクト
);
return (
<>
<button onClick={() => setUnrelated(n => n + 1)}>+1</button>
<Child list={list} filterFn={filterFn} />
</>
);
}
- 親が何回レンダーされても:
-
useCallbackは「依存が変わっていないから、前回の関数を返すね」となる -
filterFnの参照はずっと同じ(ABパターン)
-
- React.memo から見ると:
prevProps.filterFn === nextProps.filterFn // true
prevProps.list === nextProps.list // true
→ 子レンダリングをスキップ
- 子が呼ばれないので、
useMemoも heavy 処理も走らない
7. パターン早見表(雑に頭に置いておく用)
親が再レンダーされたとき(list/条件は変わっていない前提)
| パターン | 子レンダー | heavy処理 (filterなど) |
|---|---|---|
| React.memo なし / useMemo なし | ✅ 毎回 | ✅ 毎回 |
| React.memo なし / useMemo あり | ✅ 毎回 | ❌ 依存変わらなければスキップ |
| React.memo あり / useMemo なし | 🔺 関数 props が不安定なら毎回 | ✅ レンダーされたとき毎回 |
| React.memo あり / useMemo あり | 🔺 関数 props が不安定なら毎回 | 🔺 同上 |
| + useCallback(関数依存安定化)あり | ✅「本当に変わったときだけ」 | ✅「本当に変わったときだけ」 |
「関数 props が不安定かどうか」を制御するのが useCallback。
「配列/オブジェクトの結果を安定させる」のが useMemo。
8. いつ useCallback / useMemo / React.memo を使うべきか
useCallback を使うべき典型パターン
- 子コンポーネントに関数を props で渡している +
- 子が
React.memooruseMemo/useEffectの依存でその関数を見ている + - 親が頻繁に再レンダーされる(別 state のせいなど)
このとき:
-
useCallback がないと
→ 関数が毎回「違うもの扱い」になって最適化が壊れる
-
useCallback があると
→ 「本当に依存が変わったときだけ関数が変わる」状態にできる
useMemo を使うべき典型パターン
- コンポーネント内で 重い計算(1万件 filter / sort / groupBy など)をしている
- 依存が変わらない限り、その結果を再利用したい
- or その結果の配列やオブジェクトを props として渡したい(参照を安定させたい)
React.memo を使うべき典型パターン
- 子コンポーネントが「純粋な表示担当」
- 親がよく再レンダリングされるが、子に渡している props はあまり変わらない
- 「props が前回と同じなら子をスキップしたい」
9. 最後の一言まとめ
-
useState→ 値が変わると 再レンダー。値はレンダー間で保持される。
-
React.memo→ 「前回 props と今回 props を
===比較して同じなら、子コンポーネントのレンダリングを丸ごとスキップする」
-
useMemo→ レンダリングはする前提で、その中の“重い計算だけ”をキャッシュし、
依存が変わらない限り再実行しない。
-
useCallback→ 関数が毎レンダーごとに「別の関数(別クロージャ)」として作られてしまう問題に対して、
依存が変わらない限り「前回の関数オブジェクト」を返して、参照を安定させる。
→ その結果、
React.memoやuseMemoの「変化なし最適化」がちゃんと効き始める。 -
クロージャ
→ 「関数 + その関数が作られたときの外側の変数(環境)」
→ 同じコードでも、レンダリングごとに「違う環境を閉じ込めた別の関数」ができる
→ これが「人間には同じに見えるけど、エンジン的には別物」の正体。
Discussion