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.tsx や index.tsx)で、ルートの <App /> を <StrictMode> で囲む方法です。Vite で作った React プロジェクトなら、次のように最初から書かれていることも多いです。
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
createRoot(document.getElementById("root")!).render(
// App 以下のツリー全体を厳格チェックの対象にする
<StrictMode>
<App />
</StrictMode>
);
こうするとアプリ全体が対象になります。もちろんツリーの一部だけを囲むこともできます。たとえば新しく書き直した画面や、挙動が不安な機能まわりだけを重点的にチェックしたいときは、その部分だけを <StrictMode> で囲めば、その内側だけに追加チェックが効きます。
function App() {
return (
<div>
<Header />
{/* この画面のツリーだけを厳格チェックの対象にする */}
<StrictMode>
<NewFeature />
</StrictMode>
<Footer />
</div>
);
}
この例では NewFeature とその子孫にだけチェックがかかり、Header や Footer は対象外になります。
StrictMode が検出してくれること
StrictMode が有効なツリーでは、開発中の React が主に次のようなチェックを行います。いずれも「本番で起きうる、けれど気づきにくいバグ」を、開発中に前もって表面化させることが目的です。
| チェックの種類 | 内容 |
|---|---|
| 純粋でないレンダリングの検出 | コンポーネントの関数本体や一部のフックの初期化を、開発中はわざと2回呼ぶ。2回呼んでも結果が変わらないはず、という前提を崩す副作用があると異常として気づける。 |
| クリーンアップ漏れの検出 | useEffect などをマウント→アンマウント→再マウントして、購読の解除やタイマー停止といったクリーンアップが正しく書けているかを確かめる。 |
| 非推奨・レガシー API の警告 | 古い(非推奨の)React の API を使っている箇所を、コンソールの警告で知らせる。 |
純粋でないレンダリングを炙り出す
React では、コンポーネントの関数は「同じ入力に対して同じ結果を返す純粋な関数」であることが期待されます。言い換えると、レンダリング中に外部の値を書き換えたり、配列を破壊的に変更したりといった副作用を持ってはいけません。StrictMode は、この純粋さを検査するために、開発中にコンポーネントの関数本体や、useState の初期化関数などを意図的に2回呼びます。
2回呼んでも結果が同じなら問題ありません。しかし、たとえばレンダリング中に外側の変数を書き換えているようなコードは、2回呼ばれると値が二重に変わってしまい、おかしな挙動として表面化します。次はよくないコードの例です。
// レンダリングの外にある変数
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 下でマウントが繰り返されると、アンマウント後に状態を更新してしまう可能性があります。
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 からクリーンアップ関数を返し、アンマウント時にリクエストの結果を無視するようにします。ここでは中断フラグを使って、古いリクエストの結果で状態を更新しないようにしています。
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 がマウント→アンマウント→再マウントを行っても、アンマウント時にこのクリーンアップが呼ばれて ignore が true になり、古いリクエストの結果は捨てられます。イベント購読やタイマーの場合も同様で、購読の解除や clearTimeout をクリーンアップに書いておけば、二重に登録されたまま残ることはありません。StrictMode で「2回動いて困る」と感じたら、多くはこのクリーンアップを書くことで解決します。
まとめ
StrictMode は、<StrictMode> で子ツリーを囲むことで、開発モードのときだけ追加のチェックと警告を有効にするコンポーネントです。画面には何も描画せず、本番ビルドには影響しません。使い方はアプリのルートで <App /> を囲むのが基本ですが、一部のツリーだけを囲むこともできます。開発中は、純粋でないレンダリングを炙り出すためにコンポーネントの関数や一部フックの初期化を2回呼び、クリーンアップ漏れを検出するために effect をマウント→アンマウント→再マウントし、さらに非推奨 API の使用を警告します。ログが2回出たり fetch が2回走ったりして戸惑うかもしれませんが、それは開発中だけの意図的な挙動で、本番では起きません。effect のクリーンアップを正しく書けば問題にはならないので、StrictMode を外すのではなくコードを直す方向で対処しましょう。