1. ホーム
  2. React

【React】StrictMode の使い方|開発中に潜在的な問題を検出する仕組みを解説

Share

React で開発をしていると、コンソールに同じログが2回出たり、useEffect の中で書いた fetch が2回走ったりして「バグかな?」と戸惑うことがあります。その多くは <StrictMode> という仕組みが、開発中に潜在的な問題をわざと表面化させているためです。この記事では、StrictMode とは何か、どこにどう書くのか、そして「関数が2回実行される」ことで何を検出しようとしているのかを、正確な仕様に沿って初心者〜中級者向けに解説します。あわせて、StrictMode で炙り出された問題の直し方も具体的なコードで紹介します。

StrictMode とは何か

StrictMode は、囲んだ子コンポーネントのツリーに対して、開発モードでのみ追加のチェックと警告を有効にするためのコンポーネントです。React 本体が提供していて、<React.StrictMode> あるいは import { StrictMode } from "react" として使います。

大事なのは、StrictMode は画面には何も描画しないという点です。<div> のような要素を増やすわけではなく、DOM 上に痕跡を残しません。あくまで開発中の React に「このツリーは厳しめにチェックしてほしい」と伝えるためのマーカーのような存在です。そして、その追加チェックは本番ビルドでは一切実行されません。つまり、StrictMode を入れても本番の挙動やパフォーマンスには影響しないので、安心してツリーを囲めます。

アプリのルートで App を囲む

もっとも一般的な使い方は、アプリのエントリーポイント(main.tsxindex.tsx)で、ルートの <App /><StrictMode> で囲む方法です。Vite で作った React プロジェクトなら、次のように最初から書かれていることも多いです。

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

createRoot(document.getElementById("root")!).render(
  // App 以下のツリー全体を厳格チェックの対象にする
  <StrictMode>
    <App />
  </StrictMode>
);

こうするとアプリ全体が対象になります。もちろんツリーの一部だけを囲むこともできます。たとえば新しく書き直した画面や、挙動が不安な機能まわりだけを重点的にチェックしたいときは、その部分だけを <StrictMode> で囲めば、その内側だけに追加チェックが効きます。

App.tsx
function App() {
  return (
    <div>
      <Header />
      {/* この画面のツリーだけを厳格チェックの対象にする */}
      <StrictMode>
        <NewFeature />
      </StrictMode>
      <Footer />
    </div>
  );
}

この例では NewFeature とその子孫にだけチェックがかかり、HeaderFooter は対象外になります。

StrictMode が検出してくれること

StrictMode が有効なツリーでは、開発中の React が主に次のようなチェックを行います。いずれも「本番で起きうる、けれど気づきにくいバグ」を、開発中に前もって表面化させることが目的です。

チェックの種類内容
純粋でないレンダリングの検出コンポーネントの関数本体や一部のフックの初期化を、開発中はわざと2回呼ぶ。2回呼んでも結果が変わらないはず、という前提を崩す副作用があると異常として気づける。
クリーンアップ漏れの検出useEffect などをマウント→アンマウント→再マウントして、購読の解除やタイマー停止といったクリーンアップが正しく書けているかを確かめる。
非推奨・レガシー API の警告古い(非推奨の)React の API を使っている箇所を、コンソールの警告で知らせる。

純粋でないレンダリングを炙り出す

React では、コンポーネントの関数は「同じ入力に対して同じ結果を返す純粋な関数」であることが期待されます。言い換えると、レンダリング中に外部の値を書き換えたり、配列を破壊的に変更したりといった副作用を持ってはいけません。StrictMode は、この純粋さを検査するために、開発中にコンポーネントの関数本体や、useState の初期化関数などを意図的に2回呼びます

2回呼んでも結果が同じなら問題ありません。しかし、たとえばレンダリング中に外側の変数を書き換えているようなコードは、2回呼ばれると値が二重に変わってしまい、おかしな挙動として表面化します。次はよくないコードの例です。

BadCounter.tsx
// レンダリングの外にある変数
let renderCount = 0;

function BadCounter() {
  // レンダリング中に外部の状態を書き換えている(副作用=純粋でない)
  renderCount = renderCount + 1;
  return <p>描画回数: {renderCount}</p>;
}

このコンポーネントはレンダリング中に renderCount を書き換えているため純粋ではありません。StrictMode 下では関数が2回呼ばれ、カウントが想定より多く増えるので「何かおかしい」と気づけます。修正の方向としては、こうした変化する値はレンダリング中の外部変数ではなく useState などの状態として管理し、レンダリング関数自体は副作用を持たない形にします。StrictMode は、こうした設計のまずさを早い段階で教えてくれるわけです。

マウントを繰り返して effect のクリーンアップを検査する

もう一つの重要なチェックが、useEffect のクリーンアップ漏れの検出です。StrictMode 下では、コンポーネントを一度マウントした直後に、React がわざとアンマウントして、もう一度マウントし直します。このとき effect のセットアップとクリーンアップが正しく対になっていれば、購読やタイマーが二重に残ることはありません。逆に、クリーンアップを書き忘れていると、購読やタイマーが重複して動き続け、その不具合が開発中に表面化します。

この挙動はあくまで開発中の検査のためのものです。本番ビルドでは、こうした余分なマウントとアンマウントは起きません。

「2回実行される」ことに戸惑わないために

StrictMode を知らずに開発を始めると、次のような場面で驚くことがあります。

  • コンソールに書いた console.log が、なぜか毎回2回出力される
  • useEffect の中で呼んだ fetch が2回走り、ネットワークタブにリクエストが2件並ぶ
  • マウント時に1回だけ動いてほしい処理が、2回実行されているように見える

これらはバグではなく、ここまで見てきた StrictMode の検査(関数の2回呼び出しや、マウント→アンマウント→再マウント)による意図的な挙動です。重要なのは、この二重実行は開発モードのときだけで、本番ビルドでは起きないということです。そのため、開発中にログが2回出ること自体を無理に消そうとする必要はありません。

もし fetch が2回走ることが気になるなら、それは「2回動いても問題が起きないように、正しくクリーンアップを書けているか」を確認する好機です。適切にクリーンアップされていれば、2回動いても最終的な状態は正しく保たれ、本番でも安心して動きます。StrictMode をわざわざ外すより、コードのほうを正しく直すのが本筋です。

表面化した問題の直し方(クリーンアップを書く)

実例として、データ取得の effect を見てみます。次のコードはクリーンアップを書いていないため、StrictMode 下でマウントが繰り返されると、アンマウント後に状態を更新してしまう可能性があります。

UserProfile.tsx(修正前)
import { useEffect, useState } from "react";

function UserProfile({ id }: { id: string }) {
  const [name, setName] = useState("");

  useEffect(() => {
    // クリーンアップがないため、アンマウント後に setName が走る恐れがある
    fetch(`/api/users/${id}`)
      .then((res) => res.json())
      .then((data) => setName(data.name));
  }, [id]);

  return <p>{name}</p>;
}

これを直すには、effect からクリーンアップ関数を返し、アンマウント時にリクエストの結果を無視するようにします。ここでは中断フラグを使って、古いリクエストの結果で状態を更新しないようにしています。

UserProfile.tsx(修正後)
import { useEffect, useState } from "react";

function UserProfile({ id }: { id: string }) {
  const [name, setName] = useState("");

  useEffect(() => {
    let ignore = false; // この effect が古くなったかどうか

    fetch(`/api/users/${id}`)
      .then((res) => res.json())
      .then((data) => {
        // アンマウント(または id 変更)後は反映しない
        if (!ignore) setName(data.name);
      });

    // クリーンアップ関数を返す
    return () => {
      ignore = true;
    };
  }, [id]);

  return <p>{name}</p>;
}

ポイントは、useEffect のコールバックからクリーンアップ関数(return () => { ... })を返している点です。StrictMode がマウント→アンマウント→再マウントを行っても、アンマウント時にこのクリーンアップが呼ばれて ignoretrue になり、古いリクエストの結果は捨てられます。イベント購読やタイマーの場合も同様で、購読の解除や clearTimeout をクリーンアップに書いておけば、二重に登録されたまま残ることはありません。StrictMode で「2回動いて困る」と感じたら、多くはこのクリーンアップを書くことで解決します。

まとめ

StrictMode は、<StrictMode> で子ツリーを囲むことで、開発モードのときだけ追加のチェックと警告を有効にするコンポーネントです。画面には何も描画せず、本番ビルドには影響しません。使い方はアプリのルートで <App /> を囲むのが基本ですが、一部のツリーだけを囲むこともできます。開発中は、純粋でないレンダリングを炙り出すためにコンポーネントの関数や一部フックの初期化を2回呼び、クリーンアップ漏れを検出するために effect をマウント→アンマウント→再マウントし、さらに非推奨 API の使用を警告します。ログが2回出たり fetch が2回走ったりして戸惑うかもしれませんが、それは開発中だけの意図的な挙動で、本番では起きません。effect のクリーンアップを正しく書けば問題にはならないので、StrictMode を外すのではなくコードを直す方向で対処しましょう。

参考ページ