開発者の「直感」を裏切るReact Strict Mode:その「二重実行」こそが、堅牢なアーキテクチャへの登竜門だ
React開発の現場で、ふとコンソールを見たとき。「あ、APIの呼び出しが2回走っている」。この現象に遭遇して、「Reactのバグか?」と疑った経験はないだろうか?
結論から言おう。それはバグではない。Reactが君のコードの「不純さ」を見抜くために仕掛けた、極めて高度な罠であり、救済措置だ。
今回は、React Strict Modeがなぜ開発環境でわざわざコンポーネントを二重にマウントし、副作用を2回叩くのか。その背後にある深い哲学と、それを乗り越えた先に待つ、堅牢なアプリケーションの設計論について深く掘り下げていこう。
—
なぜ「二重実行」が必要なのか:イミュータビリティと純粋性の追求
React Strict Mode(`
Reactのレンダリングは、「UIは状態の関数である」という原則に基づいている。もし君のコンポーネントが、レンダリングの過程で外部の状態を書き換えたり、予期せぬ副作用を引き起こしたりすれば、Reactの再レンダリングのアルゴリズムは崩壊する。
Strict Modeが二重に実行する主な対象:
1. コンポーネントの関数本体: 純粋な計算結果を返す必要がある。
2. `useState`, `useMemo`, `useReducer` の初期化関数: 冪等(べきとう)であることが求められる。
3. `useEffect` のセットアップ関数: クリーンアップ関数とセットで、「何度実行されても結果が同じになる」ことが保証されている必要がある。
これをわざと2回実行することで、「もしこのコンポーネントがReactの都合で何度も呼び出されたら?」という極限状態をシミュレートしているのだ。ここでバグが出るコードは、将来的にReactの並行レンダリング(Concurrent Features)を導入した瞬間に、致命的なレースコンディションを引き起こす時限爆弾となる。
—
「副作用の二重実行」を正しくハンドリングする
例えば、APIからデータを取得する際、よくあるアンチパターンはこうだ。
// ❌ 悪い例:副作用の制御が甘い
useEffect(() => {
let isSubscribed = true;
fetchData().then(data => {
if (isSubscribed) setData(data); // 一見正しいようだが…
});
return () => { isSubscribed = false; };
}, []);
Strict Mode下では、この `useEffect` は2回走る。もし `fetchData` が内部でグローバルなカウンターをインクリメントしていたり、DOMを直接操作していたらどうなるか? 状態の整合性が一瞬で崩れる。
堅牢な設計:クリーンアップ関数の徹底
React 18以降のアーキテクチャでは、「副作用を一度だけ実行すること」に固執してはならない。 代わりに、「副作用をキャンセル可能にする」ことこそが正解だ。
// ✅ 良い例:AbortControllerで通信そのものを制御する
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
const response = await fetch(‘/api/data’, { signal: controller.signal });
const data = await response.json();
setData(data);
} catch (err) {
if (err.name !== ‘AbortError’) {
// エラーハンドリング
}
}
};
fetchData();
// コンポーネントがアンマウント、または再レンダリング時にキャンセル
return () => controller.abort();
}, []);
このように、Strict Modeの二重実行によって「通信が二重に発生する」ことが可視化される。それにどう対処するか? `AbortController` を使うことで、ネットワーク負荷を最小限に抑え、非同期の競合を未然に防ぐことができる。これこそが、アーキテクチャレベルでの最適化だ。
—
メモリ効率とパフォーマンスの観点から
Strict Modeは、非推奨のAPI(`findDOMNode` やレガシーな `context` など)の使用を警告する。これらは単に古いという理由だけではない。多くの場合、メモリリークや、Reactのファイバーツリーの走査効率を悪化させる原因となるからだ。
特に、`useMemo` や `useCallback` の依存配列のミスも、Strict Modeは容赦なく指摘してくる。依存関係が不完全な場合、本来キャッシュされるべき関数や値が再生成され、メモリ消費量が増大し、V8エンジンのガベージコレクションを無駄に働かせることになる。
上級エンジニアのためのチェックリスト
- `useEffect` 内で外部変数を直接変更していないか?(Refの `.current` 以外は避けるべき)
- レンダリングの計算ロジックに乱数(`Math.random()`)や `Date.now()` を直接書いていないか?(これらはレンダリングのたびに値を変化させ、結果を不確定にする)
- `useMemo` の計算コストは、キャッシュによる恩恵を上回っていないか?(過剰なメモ化は逆にメモリを食う)
—
結論:Strict Modeは「味方」である
多くのエンジニアが開発中にStrict Modeをオフにしたがるが、それは「健康診断が嫌だから病院に行かない」のと同じことだ。
Reactのレンダリングエンジンは、君たちが思っているよりもずっと速く、そしてずっと気まぐれに動いている。Strict Modeで発生する「二重の苦しみ」は、コンポーネントをより純粋に、より宣言的に書き直すための「厳格なメンター」からのフィードバックだと捉えてほしい。
このモードを通過したコードは、将来的にReactの並行機能(SuspenseやTransition)を導入する際、何ら修正することなく、堅牢に動作するはずだ。技術的負債を後回しにするのではなく、今ここで、Reactの深淵に触れる設計を実践してほしい。
さあ、エディタに戻り、コンソールを覗いてみよう。そこにはまだ、君のコードをさらに一段上のレベルへ押し上げるヒントが隠れているはずだ。

コメント