0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

StrictModeでrender・useEffectが2回走る理由

Posted at

React を開発モードかつ StrictMode で動かしていると、
「render が 2 回走る」「useEffect が 2 回実行される」という挙動に出会います。

最初はバグのように見えますが、これは React が意図的に行っている挙動です。
この記事では、

  • なぜ render が 2 回走るのか
  • なぜ useEffect まで 2 回走るのか
  • React がそれによって何を検出しようとしているのか

を、内部構造の観点から整理します。

StrictModeとは何か

以下のようにStrictModeを使うと、開発モード限定で追加のチェックが有効になります。

// main.tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.tsx";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <App />
  </StrictMode>,
);

StrictModeは「将来の React の動作(Concurrent React)」に耐えられるかを、
開発時にあぶり出すための安全装置です。

本番ビルドでは一切影響しません。

そもそも仮想DOMは?

Reactは、DOMを直接操作するのではなく、**DOMを表すJavaScriptオブジェクト(React Elementツリー)**をまず作ります。

stateが変わると、Reactは次のように動きます。

  1. state更新がスケジュールされる
  2. コンポーネント関数(例:App())が再実行される
  3. 新しいReact Elementツリーが生成される

この時点ではまだDOMには一切触れていません。

仮想DOMを使わない場合は、
「stateを変えたら、どのDOMをどう書き換えるか」
を開発者がすべて管理する必要がある。

Reactでは「stateをどう変えるか」だけを書けば、
「UIをどう変えるか」はReactが引き受ける、という設計になっています。

renderフェーズとcommitフェーズ

React の内部処理は、大きく 2 つのフェーズに分かれています。

renderフェーズ(計算)
   ↓
commitフェーズ(DOM更新)

renderフェーズとは

renderフェーズは純粋な計算のフェーズです。

ここでは以下のような処理が行われます。

  • JSXの評価
  • 条件分岐
  • stateの読み取り
  • React Element ツリーの生成

重要なのは、この段階ではDOMは一切触れられていないという点です。

そのためrenderフェーズでは、

  • DOM 操作
  • API 通信
  • setTimeout / setInterval
  • グローバル変数の変更

といった副作用を持つ処理をしてはいけません。
renderは「何度呼ばれても」「途中で捨てられても」問題ない必要があります。

commitフェーズとは

commitフェーズでは、renderフェーズの結果をもとに実際のDOMを更新します。

  • DOMノードの作成・削除
  • 属性変更
  • ref更新
  • useEffect実行

ここで初めて「外の世界」に影響が出ます。

commitフェーズは中断されず、必ず1回だけ実行されます。

commitフェーズが実行されないこともある

state を更新しても、結果が変わらなければ commit は行われません。

setState((x) => x); // 同じ値

この場合、

  • render フェーズは実行される
  • DOM に差分がないため commit は行われない

という挙動になります。

render が 2 回走る理由(StrictMode)

開発モード + StrictModeでは、Reactはrenderフェーズを意図的に2回実行(コンポーネント関数を2回呼び出し)します。

render(テスト)
render(テスト)
commit(1 回)

これはバグではなく、「renderフェーズに副作用が混ざっていないか?」を検出するためのテストです。

renderは「計算」であるべきなので、

  • 何回実行しても同じ結果になるか
  • 実行回数に依存していないか
  • state → UIの対応が壊れていないか
    をReactが確認しています。

ダメな例(render 中の副作用)

function App() {
  fetch("/api/data"); // ❌ render中の副作用
  return <h1>Hello</h1>;
}

StrictMode では、renderが2回走るため、APIリクエストも2回飛びます。
これは「将来Concurrent Renderingで壊れるコード」です。

正しい例

function App() {
  return <h1>Hello</h1>;
}

useEffect(() => {
  fetch("/api/data"); // ✅ commit後
}, []);

副作用はcommitフェーズ(useEffect)に寄せます。

useEffectが2回走る理由

StrictMode では、useEffect も 2 回実行されます。

正確には、次の流れが意図的にシミュレートされます。

mount
  → useEffect 実行
  → cleanup 実行
unmount
mount
  → useEffect 実行

これは「副作用が正しく後始末(cleanup)できているか」を検証するためです。

そもそも useEffect は何のため?

useEffect は DOM 更新後に、React の外の世界と同期するための場所
例:

  • API 通信
  • setInterval
  • WebSocket
  • localStorage

React が恐れている最悪のバグ

React が最も警戒しているのは、「副作用を登録したのに、解除し忘れること」です。

例えば次のコードは危険です。

useEffect(() => {
  window.addEventListener("resize", onResize);
}, []);

cleanup がないため、
再マウント時にイベントが二重登録されます。

OKな例

useEffect(() => {
  window.addEventListener("resize", onResize);

  return () => {
    window.removeEventListener("resize", onResize);
  };
}, []);

StrictModeのmount→cleanup→mountという流れでも安全に動作します。

StrictMode は「わざと壊しに来る」

  • renderを何度も呼ぶ
  • effectを即アンマウントする
  • 開発者の想定を裏切る
    という形で「このコードは本当に安全?」を強制的に試します。

まとめ

  • renderが2回走るのはrenderが純粋関数であるかの検証
  • useEffectが2回走るのはcleanupが正しくかけているかの検証
  • どちらも開発モード限定
  • 本番ビルドでは起きない
  • StrictModeは「将来のReactに耐えられるコードか」を見るための仕組み

StrictModeで壊れるコードは、将来のReactでは本当に壊れる可能性が高いコードです。
「うるさい挙動」ではなく、React からの警告として受け取るのが正解です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?