本文へスキップ
ウェブエンジニア問題集
第3章

Reactのレンダー・マウント・再レンダー・アンマウントの違い

約12分
この章の目次開く

Reactのフック(useState・useEffect など)を正しく使うには、レンダーとマウントの違いを理解しておく必要があります。 この2つは混同されやすいですが、指しているものがまったく異なります。

学習者学習者

「レンダー」と「マウント」って同じ意味じゃないの…?なんとなくで使ってたけど、違うものなの?


まず結論

Reactで「レンダーされた」と「マウントされた」は同じ意味ではありません。レンダーは関数が呼ばれてJSXを計算すること、マウントはその結果が初めてDOMに入ることです。

状況起きること
初めて画面に出る初回レンダー → DOMに挿入 → マウント後の useEffect
stateが変わる再レンダー → DOMの差分更新
propsが変わる再レンダー → DOMの差分更新
条件分岐で消えるアンマウント → useEffect のクリーンアップ

この区別が分かると、useEffect(..., []) がなぜ初回だけ実行されるのか、state更新でなぜ関数全体が再実行されるのかを説明できます。


4つの用語

用語意味
レンダーコンポーネントの関数が呼ばれてUIを計算すること
マウントコンポーネントが最初にDOMに追加されること
再レンダー2回目以降のレンダー
アンマウントコンポーネントがDOMから取り除かれること

レンダーとは

レンダーとは、Reactがコンポーネント関数を呼び出してUIの計算結果(JSX)を得ることです。

function Greeting({ name }: { name: string }) {
  // この関数が呼ばれること自体が「レンダー」
  return <p>こんにちは、{name}さん</p>;
}
tsx

レンダーのたびに関数全体が再実行されます。const・let で定義したローカル変数も、すべて作り直されます。

レンダーが起きるタイミング

  • 自身のstateが変わったとき(useState の更新関数を呼んだとき)
  • 親コンポーネントが再レンダーされたとき(子も連動して再実行される)
function Counter() {
  const [count, setCount] = useState(0);
 
  console.log('rendered'); // ボタンを押すたびにここが実行される
 
  return <button onClick={() => setCount((c) => c + 1)}>{count}</button>;
}
tsx

ボタンを押すたびに Counter 関数が再実行され、console.log('rendered') が出力されます。


マウントとは

マウントとは、コンポーネントが初めてDOMに挿入されることです。ライフサイクル中に1回だけ起きます。

マウントは「初回レンダー + DOMへの挿入」の組み合わせです。

マウント = 初回レンダー + DOMへの挿入

コンポーネントがページに表示されるとき、Reactは次の順序で処理します。

  1. コンポーネント関数を呼ぶ(= 初回レンダー)
  2. 戻り値のJSXをDOMに挿入(= マウント完了)
  3. useEffect のコールバックを実行(マウント後)

マウント = 画面リロード?

学習者学習者

マウントって、ページを再読み込みしたときのことでしょ?

これはよくある誤解です。画面リロードでマウントは起きますが、マウントは画面リロードだけで起きるものではありません。

集合の包含関係で整理すると、次のようになります。

レンダー ⊃ マウント ⊃ 画面リロード
用語範囲
レンダーマウント + 再レンダーを含むすべて
マウント画面リロード + それ以外のマウントを含む
画面リロードマウントが起きる場面の1つにすぎない

マウントが起きる場面

画面リロードは、アプリ全体をゼロから作り直すため全コンポーネントがマウントされます。しかしマウントはそれだけではありません。

function App() {
  const [show, setShow] = useState(false);
 
  return (
    <>
      <button onClick={() => setShow(!show)}>toggle</button>
      {show && <Child />}
    </>
  );
}
tsx

この例では、ボタンを押して show が true になるたびに <Child /> がDOMに追加され、マウントが起きます。false になればアンマウントされ、再び true になれば新しいインスタンスとして再びマウントされます。

ページを再読み込みしなくても、以下の場面でマウントは発生します。

  • 条件分岐で表示されたとき — {show && <Component />}
  • ルート遷移で新しいページに移動したとき — 遷移先のコンポーネントがマウントされる
  • key が変わったとき — Reactが別のインスタンスとして作り直す

useEffectの依存配列との対応

学習者学習者

条件分岐で表示したコンポーネントも毎回マウントされるなら、useEffect(..., []) は何回も実行されるってこと?

先生先生

そのとおり。[] は「マウント時に1回だけ」だけど、コンポーネントが消えて再び表示されればまた新しいマウントだから、そのたびに実行されるよ。

どのパターンでもマウント時は必ず実行されるのが共通ルールです。

依存配列マウント時再レンダー時
[]実行される実行されない
[a, b]実行されるa か b が変わった時だけ実行
なし実行される毎回実行される

useEffect(..., []) が「1回だけ」と言われるのは、再レンダーでは実行されず、マウント時だけ実行されるためです。そして画面リロードに限らず、条件分岐やルート遷移でコンポーネントが表示されるたびにマウントは起きるため、[] でも複数回実行されることがあります。


再レンダーとアンマウント

ライフサイクルの全体像をひらめくイメージ

再レンダー — stateやpropsの変化をきっかけに、マウント後に繰り返されるレンダーです。DOMへの挿入は最初の1回だけで、再レンダーではReactが差分だけを更新します。

アンマウント — コンポーネントが画面から取り除かれることです。条件分岐({show && <Component />})やルート遷移などで発生します。アンマウント時に useEffect のクリーンアップ関数が実行されます。


全体の流れ


コードで確認する

function Counter() {
  const [count, setCount] = useState(0);
 
  // レンダーのたびにここが実行される(初回 + 再レンダー)
  console.log('rendered');
 
  useEffect(() => {
    // マウント時に1回だけ実行される
    console.log('mounted');
 
    return () => {
      // アンマウント時に実行される
      console.log('unmounted');
    };
  }, []);
 
  return <button onClick={() => setCount((c) => c + 1)}>{count}</button>;
}
tsx

ボタンを3回押したときのコンソール出力:

rendered   ← 初回レンダー(マウント時)
mounted    ← マウント完了後
rendered   ← 再レンダー(1回目)
rendered   ← 再レンダー(2回目)
rendered   ← 再レンダー(3回目)

rendered は計4回(初回 + 3回)、mounted は1回だけです。コンポーネントが画面から消えるとき(アンマウント)に unmounted が出力されます。

学習者学習者

自分のPCで動かしたら、最初に rendered が2回、mounted → unmounted → mounted って出たんだけど…?

これは開発環境でStrict Modeが有効になっているためです。上の出力はStrict Modeが無効な場合(本番ビルドと同じ動き)のものです。理由は次の節で説明します。


Strict Modeで2回実行されるのはなぜ

Strict Modeは、バグの原因になりやすい書き方を開発中に見つけるためのReactの機能です。有効にすると、Reactは開発環境でだけ、次の処理をわざと余分に1回ずつ実行します(StrictMode — React公式リファレンス)。

余分に実行されるもの見つけたいバグ
コンポーネント関数(レンダー)レンダー中に外部の値を書き換えている(純粋でない)
useEffect のセットアップとクリーンアップクリーンアップを書き忘れている
コールバック ref のセットアップとクリーンアップref のクリーンアップを書き忘れている

公式リファレンスは、これらのチェックは開発時のみで、本番ビルドには影響しないと明記しています。

先ほどのCounterで起きていること

Strict Modeが有効だと、マウント時の処理は次の順に進みます。

  1. コンポーネント関数を2回呼ぶ → rendered が2回出る
  2. DOMに挿入する
  3. useEffect のセットアップを実行する → mounted
  4. すぐにクリーンアップを実行する → unmounted
  5. もう一度セットアップを実行する → mounted
rendered
rendered
mounted
unmounted
mounted

4と5は、「一度アンマウントして、もう一度マウントした」状況を開発中に再現しています。前の節で見たとおり、コンポーネントは条件分岐やルート遷移で何度でもマウントし直されます。setup → cleanup → setup の順で実行しても正しく動くなら、実際に何度マウントされても壊れない、という確認になります。

学習者学習者

じゃあ、2回実行されて困るならStrict Modeを切ればいいの?

先生先生

切る前に、クリーンアップを書けているか見直そう。2回目で壊れるなら、本番で再マウントされたときにも同じ壊れ方をするんだ。

たとえば useEffect の中でイベントリスナーを登録してクリーンアップで外していないと、Strict Modeでは登録が2重になり、すぐに気づけます。クリーンアップを正しく書けば、setup → cleanup → setup のあとも登録は1つだけになります。

Strict Modeを有効にする場所


useEffectとの関係

この違いが useEffect の依存配列と直接つながります。

// 依存配列なし → 毎レンダー後に実行(再レンダーのたびにも走る)
useEffect(() => {
  console.log('every render');
});
 
// 空配列 → マウント時に1回だけ実行
useEffect(() => {
  console.log('mount only');
}, []);
tsx

「依存配列なし」と「空配列 []」は見た目が似ていますが、前者は毎レンダー後に実行され、後者はマウント時のみです。この違いはレンダーとマウントの概念を理解していれば自然に納得できます。

詳しくは「useEffect — 副作用と外部同期」で解説しています。


まとめ

  • レンダー — コンポーネント関数の実行。stateやpropsの変化のたびに何度でも起きる。
  • マウント — 初回レンダー + DOMへの挿入。ライフサイクル中に1回だけ。
  • 再レンダー — 2回目以降のレンダー。DOMは差分だけ更新される。
  • アンマウント — コンポーネントがDOMから取り除かれるとき。
  • Strict Mode — 開発環境でだけ、レンダーと useEffect をわざと余分に実行してバグを見つける。本番ビルドには影響しない。

useEffect(..., []) が「マウント時のみ」と言われるのは、空配列だと何にも反応しないため最初の1回しか実行されないからです。レンダーとマウントの違いを押さえると、フックの動作が一気にわかりやすくなります。


参考

Reactクイズに挑戦するレンダー、マウント、再レンダー、アンマウント、StrictModeやhydrationの考え方を4択クイズで確認しよう