🐕

useState / useMemo / useCallback / React.memo / クロージャー の理解を深める

に公開

このメモのゴール

  • 「なぜ再レンダリングが起こるのか」
  • useState / useMemo / useCallback / React.memo がそれぞれ何をしているのか」
  • クロージャ参照の安定 が、React の最適化にどう関係するのか」
  • 「いつ useCallback が本当に必要で、いついらないのか」

を、自分で説明できるレベルで整理する。


1. React の再レンダリングの基本

再レンダリングが起こるきっかけ

const [state, setState] = useState(initial);

  • setState(...) を呼ぶと、コンポーネントの再レンダリングが予約される
  • その後、React がコンポーネント関数を もう一度最初から実行 する。

流れイメージ:

  1. setState(newValue) を呼ぶ
  2. React がそのコンポーネントの「再レンダー」をスケジュール
  3. 再レンダー中に
    • useState が新しい値を返す
    • useCallback / useMemo が依存配列をチェック
    • JSX を return
  4. 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

  • counter1count=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. useMemoReact.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.memo or useMemo / useEffect の依存でその関数を見ている +
  • 親が頻繁に再レンダーされる(別 state のせいなど)

このとき:

  • useCallback がないと

    → 関数が毎回「違うもの扱い」になって最適化が壊れる

  • useCallback があると

    → 「本当に依存が変わったときだけ関数が変わる」状態にできる

useMemo を使うべき典型パターン

  • コンポーネント内で 重い計算(1万件 filter / sort / groupBy など)をしている
  • 依存が変わらない限り、その結果を再利用したい
  • or その結果の配列やオブジェクトを props として渡したい(参照を安定させたい)

React.memo を使うべき典型パターン

  • 子コンポーネントが「純粋な表示担当」
  • 親がよく再レンダリングされるが、子に渡している props はあまり変わらない
  • 「props が前回と同じなら子をスキップしたい」

9. 最後の一言まとめ

  • useState

    → 値が変わると 再レンダー。値はレンダー間で保持される。

  • React.memo

    → 「前回 props と今回 props を === 比較して同じなら、

    子コンポーネントのレンダリングを丸ごとスキップする」

  • useMemo

    レンダリングはする前提で、その中の“重い計算だけ”をキャッシュし、

    依存が変わらない限り再実行しない。

  • useCallback

    → 関数が毎レンダーごとに「別の関数(別クロージャ)」として作られてしまう問題に対して、

    依存が変わらない限り「前回の関数オブジェクト」を返して、参照を安定させる

    → その結果、React.memouseMemo の「変化なし最適化」がちゃんと効き始める。

  • クロージャ

    → 「関数 + その関数が作られたときの外側の変数(環境)」

    → 同じコードでも、レンダリングごとに「違う環境を閉じ込めた別の関数」ができる

    → これが「人間には同じに見えるけど、エンジン的には別物」の正体。


Discussion