こんにちは。プロダクトの規模が膨らみ、チームメンバーのレビューに追われる日々を送るシニアエンジニアの私だ。
さて、君も最近こんな経験をしていないかい?
「あれ? 開発環境でアプリを立ち上げたら、`useEffect` の中身が謎に2回走るんだけど……俺のコード、なんかバグってる?」
……安心してほしい。君のコードは壊れていない。犯人はReact 18の「Strict Mode(厳格モード)」だ。
今回は、中級からもう一歩上の「本当の意味でReactを使いこなすエンジニア」になるために、このStrict Modeが裏側で何をやっているのか、そして`useState`や副作用の初期化において私たちがどう立ち回るべきなのかを、現場のリアルな温度感でお伝えしよう。
—
1. なぜReactはわざわざコンポーネントを2回実行するのか?
まず大前提として、この「二重レンダリング(厳密にはマウント・アンマウント・再マウントのシミュレーション)」は、開発環境(`development`)のStrict Modeでだけ発生する意図された挙動だ。本番環境(`production`)ではこんなことは起きない。
「じゃあ、なんでわざわざそんなまどろっこしいことすんの?」って思うよな。
理由はシンプルで、「将来のReact(Concurrent Features等)が当たり前になる世界に向けて、お前のコンポーネントが『何度でも安全に再実行できる(Idempotent:べき等な)設計』になっているかストレステストしてやるよ」というReactチームからの愛のムチだ。
ブラウザの裏側で何が起きているかというと、Reactは開発時において以下のライフサイクルをあえて高速でシミュレートする。
1. コンポーネントがマウントされる
2. (即座に)一度アンマウントされる
3. もう一度マウントされる
この仕様を知らないと、「え、なんでAPIが2回叩かれるの?」「`useState`の初期化関数が2回呼ばれたんだけど?」とパニックになり、無駄な`useRef`ハックでコードを汚すという、現場でよくあるアンチパターンにハマるわけだ。
—
2. `useState` の初期化と二重実行の罠
特に注意したいのが、`useState`に値を渡すときだ。直感的に重い処理や初期計算を書きがちだが、ここに落とし穴がある。
// ❌ 良くない例:毎回のレンダリングで重い処理が走ってしまう
const [data, setData] = useState(expensiveCalculation());
JavaScriptの基本として、上記のように書くと、コンポーネントが再描画されるたびに(たとえ初期値が使われ捨てられるとしても)`expensiveCalculation()` が評価される。そのため、初期化時は遅延初期化(Lazy Initialization)を使うのがReactの基本だ。
// ⭕ 良い例:関数を渡すことで、初回マウント時のみ実行される
const [data, setData] = useState(() => expensiveCalculation());
しかし、ここでStrict Modeの二重実行が絡んでくる。この遅延初期化用の関数、Strict Mode下では開発時に2回呼ばれることがある。
もしこの初期化関数の中に「外部のミュータブルな変数を書き換える」「グローバルなカウンターをインクリメントする」といった副作用(Side Effect)を混ぜていたらどうなるか? 開発環境だけで値がズレる、原因不明のキモいバグの出来上がりだ。
鉄則:`useState` の初期化関数は、純粋関数(Pure Function)でなければならない。 副作用は絶対に持ち込むな。これ、今日のテストに出すから覚えておくように。
—
3. 実務で使える!堅牢な状態初期化と副作用のサンプルコード
理屈はこれくらいにして、現場でそのまま使えるクリーンなコードを見ていこう。
今回は、ローカルストレージや何らかの高コストな計算結果を安全に`useState`で初期化しつつ、Strict Modeの二重実行にも耐えるコンポーネントの実装例だ。
import React, { useState, useEffect } from ‘react’;
// 【模擬的な高コストな初期化処理】
// 注意:この関数は「純粋」である必要があります。外部の状態を汚染してはいけません。
const getInitialSettings = () => {
console.log(‘[Strict Mode確認用] 初期化関数が実行されました’);
// 例として localStorage からデータを取得する処理を想定
const savedTheme = localStorage.getItem(‘app-theme’);
if (savedTheme) {
return { theme: savedTheme, loadedAt: new Date().toISOString() };
}
// デフォルト値
return { theme: ‘light’, loadedAt: new Date().toISOString() };
};
export const UserDashboard: React.FC = () => {
// ① 遅延初期化のベストプラクティス
// アロー関数を渡すことで、初期マウント時(Strict Modeでは検証のため2回呼ばれる)のみ評価されます。
const [settings, setSettings] = useState(() => getInitialSettings());
// ② 副作用のハンドリング
// Strict Modeでの二重マウントに対応するため、クリーンアップ関数を正しく書くことが重要です。
useEffect(() => {
console.log(‘[useEffect] 副作用が実行されました(マウント時)’);
// 例:何らかのイベントリスナーの登録や、Websocketの接続など
const handleStorageChange = (event: StorageEvent) => {
if (event.key === ‘app-theme’ && event.newValue) {
setSettings((prev) => ({ …prev, theme: event.newValue! }));
}
};
window.addEventListener(‘storage’, handleStorageChange);
// 【重要】二重マウント・アンマウントのシミュレーションに対応するクリーンアップ
// Strict Modeでは、このクリーンアップが即座に呼ばれた後、もう一度マウントされます。
return () => {
console.log(‘[useEffect クリーンアップ] アンマウント処理が実行されました’);
window.removeEventListener(‘storage’, handleStorageChange);
};
}, []); // 依存配列が空なので、マウント・アンマウント時にのみ走る
return (
ダッシュボード
現在のテーマ: {settings.theme}
初期化読み込み時刻: {settings.loadedAt}
);
};
—
4. シニアからのアドバイス:Strict Modeを敵視するな
現場でよくあるアンチパターンとして、二重レンダリングやエフェクトの2回実行が鬱陶しいからといって、`index.tsx` (または `main.tsx`) から `
絶対にそれだけはやめてくれ。
Strict Modeは、私たちが書いたコードの「ほころび」を事前にあぶり出してくれる優秀な検問所だ。ここで2回実行されて壊れるコードは、遅かれ早かれ本番環境のユーザーの元で、画面のちらつきやメモリリーク、予期せぬAPIの重複叩きといったクリティカルなバグとして爆発する。
もしStrict Modeで挙動がおかしくなる箇所を見つけたら、「Reactのバグだ」と疑う前に、自分たちのコードを振り返ろう。
- `useEffect` のクリーンアップをサボっていないか?
- `useState` の初期化やレンダリング中に、不要な副作用(外部変数の書き換えなど)を起こしていないか?
- 依存配列(dependency array)の指定漏れはないか?
これらを一つずつクリアしていくことこそが、モダンなReact開発における最大のスキルアップの近道だ。
さあ、今日のコードレビューからは、Strict Modeの動きを味方につけた美しい設計のコードを書いていこうぜ。何か質問があれば、いつでも私のデスクまで聞きに来てくれ。

コメント