Reactアプリケーションを開発していると、「あれ?この処理、なんで2回実行されるんだろう?」と首を傾げることはありませんか?特にuseEffectフックを使っていると、開発環境で予期せぬ動作に遭遇しがちです。しかし、これはReactが意図的に行っている動作であり、正しく理解すれば、より堅牢でバグの少ないアプリケーションを構築するためのヒントになります。

本記事では、Reactコンポーネントが2回呼ばれる・実行される現象の深層に迫り、その原因、そして効果的な対策までを徹底的に解説します。この記事を読めば、もうこの現象に悩まされることはなくなるでしょう。

  1. Reactコンポーネントが2回レンダリングされるのはなぜ?
    1. 1.1. レンダリングフェーズとコミットフェーズの基本
    2. 1.2. Concurrent Renderingの影響
    3. 1.3. なぜReactは複数回レンダリングするのか?その意図
  2. useEffectの挙動とdependency arrayの重要性
    1. 2.1. useEffectの基本的な役割とライフサイクル
    2. 2.2. dependency arrayの役割と省略時の挙動
    3. 2.3. 不適切な依存配列が引き起こす問題点
  3. Strict Modeが引き起こす2回実行の可能性
    1. 3.1. Strict Modeの目的と機能
    2. 3.2. React 18以降のStrict Modeにおける具体的な挙動
    3. 3.3. Strict Modeが教えてくれる「堅牢なコード」の書き方
  4. パフォーマンス改善とデバッグのポイント
    1. 4.1. useEffectのクリーンアップ関数の徹底活用
    2. 4.2. APIリクエストの重複実行を防ぐ実用的なテクニック
    3. 4.3. React Developer Toolsを活用したデバッグ手法
  5. よくあるエラー(404, 500, 502)との関連性
    1. 5.1. 2回実行が引き起こす間接的なAPIエラー(404, 500)
    2. 5.2. サーバーサイドレンダリング(SSR)環境での注意点
    3. 5.3. 効果的なエラーハンドリングと予防策
  6. まとめ
  7. よくある質問
    1. Q: Reactコンポーネントが2回レンダリングされる主な原因は何ですか?
    2. Q: useEffectでdependency arrayを空にするとどうなりますか?
    3. Q: Strict Modeとは何ですか? 2回実行とどう関係がありますか?
    4. Q: コンポーネントが2回実行されるのを防ぐにはどうすれば良いですか?
    5. Q: 404エラーや500エラーなど、他のReactエラーとはどのように区別されますか?

Reactコンポーネントが2回レンダリングされるのはなぜ?

1.1. レンダリングフェーズとコミットフェーズの基本

Reactコンポーネントのレンダリングは、大きく「レンダリングフェーズ」と「コミットフェーズ」の2段階で進行します。まず、レンダリングフェーズでは、JSXコードがJavaScriptオブジェクトであるVirtual DOMに変換されます。

この段階では、Reactは現在のVirtual DOMと前回のVirtual DOMを比較し、UIにどのような変更が必要かを特定します(このプロセスをReconciliationまたはDiffingと呼びます)。重要なのは、このフェーズではまだ実際のDOM操作は行われず、UIへの変更は「準備」の段階にあるということです。

次に、コミットフェーズで、レンダリングフェーズで特定された変更点が実際のDOMに適用されます。ここで初めて、ユーザーが目にするUIに変化が反映されます。

DOMの更新後、useLayoutEffectのような同期的なフックが実行され、その後、useEffectのような非同期的なフック(Passive Effects)が実行される流れです。このように、レンダリングは複数のステップを経て行われるため、途中で様々な要因によって処理が複数回行われる可能性があります。(参考情報:Reactのレンダリングフェーズとコミットフェーズ)

1.2. Concurrent Renderingの影響

React 18から導入されたConcurrent Rendering(同時実行レンダリング)は、Reactが複数のレンダリング処理を同時に、または中断・再開を繰り返しながら実行できる仕組みです。

この機能により、UIの応答性を維持しながら、大量のデータ処理や複雑なレンダリングをバックグラウンドで効率的に行えるようになりました。しかし、この柔軟性が「コンポーネントが複数回レンダリングされる」という現象の一因となることがあります。

Concurrent Renderingでは、レンダリングフェーズが途中で中断され、優先度の高い更新が割り込んだ後、再び再開されることがあります。これにより、同じコンポーネントのレンダリング処理が複数回走る可能性が生じるのです。ただし、これはあくまでUIの準備段階の話であり、実際のDOMへのコミットは一度だけ行われることがほとんどです。

開発中にコンポーネントが複数回レンダリングされるのを目にしても、必ずしもバグとは限りません。Reactがパフォーマンス最適化のために行っている動作であることが多いのです。(参考情報:ReactのレンダリングフェーズのConcurrent Rendering)

1.3. なぜReactは複数回レンダリングするのか?その意図

Reactがコンポーネントを複数回レンダリングする可能性を持つのは、開発者がより堅牢でパフォーマンスの高いアプリケーションを構築できるようにするためです。特に、Strict Mode有効時の意図的な複数回実行は、開発中に潜在的なバグを早期に発見することを目的としています。

例えば、副作用(Side Effect)を持つコードがコンポーネントのライフサイクルと適切に連動しているかをテストする際、複数回の実行は非常に有効です。具体的には、useEffectフックのセットアップ関数とクリーンアップ関数が正しく対になっているか、つまり、セットアップで行った処理がクリーンアップで確実に元に戻されるかを確認するためです。

このアプローチにより、開発者は意図しないリソースリークやメモリリーク、イベントリスナーの重複登録といった問題を未然に防ぐことができます。一見すると無駄な動作に見えるかもしれませんが、これは品質の高いアプリケーションを本番環境にデプロイするための重要な安全策なのです。この意図を理解することで、開発中の挙動に対する見方も変わるはずです。(参考情報:Strict Modeの目的)

useEffectの挙動とdependency arrayの重要性

2.1. useEffectの基本的な役割とライフサイクル

useEffectフックは、Reactコンポーネント内で副作用(Side Effects)を管理するための非常に強力なツールです。副作用とは、データの取得(APIコール)、DOMの直接操作、イベントリスナーの設定、タイマーの設定、サブスクリプションの購読などが挙げられます。

このフックは、コンポーネントが「マウントされた時(初回レンダリング後)」、「更新された時(依存配列内の値が変更された時)」、そして「アンマウントされる時(クリーンアップ関数)」という、Reactコンポーネントの主要なライフサイクルイベントに同期して実行されます。

例えば、コンポーネントが画面に表示された直後に一度だけAPIからデータを取得したい場合、useEffectの第二引数(依存配列)を空の配列[]として設定します。これにより、コンポーネントのマウント時に一度だけデータ取得処理が実行され、その後は再レンダリングされても再度実行されることはありません。

useEffectを適切に使うことで、コンポーネントの純粋性を保ちつつ、外部との連携をスムーズに行うことが可能になります。(参考情報:useEffectフックの一般的な説明)

2.2. dependency arrayの役割と省略時の挙動

useEffectフックの第二引数である依存配列(Dependency Array)は、エフェクトの実行タイミングを制御する上で非常に重要な役割を果たします。この配列には、エフェクトの内部で使用しているすべてのリアクティブな値(propsstate、コンポーネント内で定義された関数など)を含める必要があります。

依存配列を指定することで、Reactは配列内の値が前回のレンダリング時と異なっている場合にのみ、エフェクトを再実行します。これにより、不要な再実行を防ぎ、パフォーマンスを向上させることができます。

  • 依存配列が空の[]の場合:エフェクトはコンポーネントがマウントされた時に一度だけ実行され、アンマウント時にクリーンアップ関数が実行されます。
  • 依存配列を省略した場合:エフェクトはコンポーネントがレンダリングされるたびに毎回実行されます。これは意図しない無限ループやパフォーマンス低下の原因となることが多いため、基本的には推奨されません。

ESLintのreact-hooks/exhaustive-depsルールは、依存配列に不足がある場合に警告を発してくれるため、このルールを有効にして開発することをお勧めします。(参考情報:依存配列の欠落または誤り)

2.3. 不適切な依存配列が引き起こす問題点

依存配列を適切に管理しないと、アプリケーションに様々な問題を引き起こす可能性があります。最も一般的な問題は、無限ループです。

例えば、useEffect内で状態を更新するsetStateを呼び出し、その状態が依存配列に含まれていない、または誤って指定されている場合、状態更新がコンポーネントの再レンダリングを引き起こし、再びuseEffectが実行され、さらに状態が更新される、という無限ループに陥ることがあります。

また、依存配列に不足がある場合、エフェクトが古いpropsstateの値を参照してしまう「古いクロージャ」問題が発生することがあります。これにより、UIに表示されるデータと実際のアプリケーションロジックが同期せず、予期せぬ動作やバグにつながります。

さらに、不要な再実行はパフォーマンスの低下を招きます。例えば、APIリクエストを何度も送信してしまったり、重い計算を繰り返し実行してしまったりすることで、アプリケーションの応答性が悪化し、ユーザーエクスペリエンスを損ねる可能性があります。これらの問題を避けるためにも、依存配列の正確な管理はReact開発の基本中の基本と言えるでしょう。(参考情報:依存配列の欠落または誤り、エフェクト内でのState更新)

Strict Modeが引き起こす2回実行の可能性

3.1. Strict Modeの目的と機能

ReactのStrict Mode(厳格モード)は、開発中に潜在的な問題やバグを検出するための強力なツールです。本番環境での動作には影響を与えず、開発環境でのみ有効になります。その主な目的は、将来のReactの変更に備えたり、アプリケーションコードの安定性と堅牢性を高めるためのアドバイスを提供することにあります。

Strict Modeが有効になっていると、Reactはコンポーネントの特定の部分を意図的に複数回実行したり、特定のライフサイクルメソッドに警告を発したりします。これは、副作用が正しく処理されているか、コンポーネントが予期しない動作をしないかを検証するためです。

例えば、純粋な関数であるべきコンポーネントが、レンダリング中に副作用を起こしていないかなどをチェックします。これにより、開発者は本番環境で問題が発生する前に、コードの改善点を発見しやすくなります。Strict Modeは、開発者にとって「信頼できるメンター」のような存在と言えるでしょう。(参考情報:Strict Modeの目的)

3.2. React 18以降のStrict Modeにおける具体的な挙動

React 18以降のStrict Modeでは、useEffectフックの動作に関して、特に注目すべき変更が加えられました。コンポーネントが初めてマウントされる際、意図的に一度アンマウントしてから再度マウントするという追加のチェックが行われます。

この「アンマウント → マウント」のサイクルにより、useEffectフックは初期レンダリング時に2回実行されることになります。具体的には、初回マウント時にuseEffectのセットアップ関数が実行され、その後すぐにクリーンアップ関数が実行され、再びセットアップ関数が実行されるという流れです。

この挙動の狙いは、useEffectのセットアップ関数とクリーンアップ関数が正しく対になっているかを開発中に検証することにあります。例えば、イベントリスナーを登録した場合、クリーンアップ関数でそれが正しく解除されているかを確認できます。もし解除処理が漏れていれば、2回目のセットアップ時にエラーが発生したり、重複してリスナーが登録されてしまうことで問題が顕在化します。

繰り返しになりますが、この動作は開発環境でのみ発生し、本番環境ではuseEffectは期待通り一度だけ実行されますのでご安心ください。(参考情報:React 18以降の挙動)

3.3. Strict Modeが教えてくれる「堅牢なコード」の書き方

Strict ModeによるuseEffectの2回実行は、単なる煩わしい挙動ではなく、より堅牢で信頼性の高いReactアプリケーションを構築するための重要な「教訓」を私たちに与えてくれます。

これは、「副作用を持つ処理は、必ずその副作用を元に戻すクリーンアップ処理とセットで考えるべきである」というReactの設計思想を強く反映しています。イベントリスナーの登録、タイマーの設定、APIリクエストの開始など、あらゆる副作用には、それらを「クリーンアップ」するための対応する処理が必要です。

Strict Modeは、このクリーンアップ処理が正しく実装されているかを開発段階で積極的にテストし、問題があれば早期にフィードバックしてくれます。例えば、APIリクエストの途中でコンポーネントがアンマウントされた場合に、そのリクエストをキャンセルする仕組み(AbortControllerなど)がなければ、不必要な処理がバックグラウンドで走り続ける可能性があります。

このように、Strict Modeを有効にして開発を進めることは、将来的なバグの発生を防ぎ、アプリケーションのパフォーマンスと安定性を高める上で非常に有益です。厳格なテストを通じて、より高品質なコードを書く習慣を身につけることができるでしょう。(参考情報:Strict Modeの目的、クリーンアップ関数の実装)

パフォーマンス改善とデバッグのポイント

4.1. useEffectのクリーンアップ関数の徹底活用

useEffectのクリーンアップ関数は、副作用を適切に「元に戻す」ための非常に重要な仕組みです。特に、Strict Modeが2回実行をトリガーする状況では、このクリーンアップ関数の有無がアプリケーションの安定性を大きく左右します。

クリーンアップ関数は、エフェクトが再実行される前、またはコンポーネントがアンマウントされる直前に呼び出されます。具体的には、以下のような処理に活用できます。

  • イベントリスナーの解除addEventListenerで登録したリスナーは、removeEventListenerで必ず解除します。
  • タイマーのクリアsetTimeoutsetIntervalで設定したタイマーは、clearTimeoutclearIntervalで停止します。
  • APIリクエストのキャンセル:進行中のAPIリクエストは、AbortControllerなどを利用してキャンセルします。
  • サブスクリプションの解除:外部ライブラリやWebSocketなどのサブスクリプションは、適切に解除します。

クリーンアップ関数を実装することで、メモリリーク、イベントの重複登録、意図しないAPIコールといった問題を防ぎ、アプリケーションの健全性を保つことができます。これは、React開発における基本的なベストプラクティスの一つです。(参考情報:クリーンアップ関数の実装)

4.2. APIリクエストの重複実行を防ぐ実用的なテクニック

useEffectが複数回実行されると、特にAPIリクエストのようなネットワーク通信を伴う処理では、意図しない重複リクエストが発生しやすくなります。これを防ぐためには、いくつかの実用的なテクニックがあります。

  1. AbortControllerの利用:これはFetch APIと組み合わせて使用し、進行中のリクエストをキャンセルする標準的な方法です。useEffectのクリーンアップ関数内でcontroller.abort()を呼び出すことで、前のリクエストが完了する前に新しいリクエストが開始されるのを防ぎます。
  2. useRefを活用したフラグ管理:Strict Modeのアンマウント・再マウントサイクルに対応するわけではありませんが、一般的な重複実行防止策として、useRefでフラグを管理し、特定のリクエストが既に実行中であるか、または一度実行済みであるかを追跡する方法があります。これにより、useEffectが再実行されても、フラグをチェックして重複処理をスキップできます。
  3. キャッシュ機構の導入:データ取得系のAPIリクエストの場合、フェッチされたデータをローカルでキャッシュする仕組みを導入することで、2回目のリクエスト時にはネットワークにアクセスせず、キャッシュされたデータを返すようにできます。これはカスタムフックとして実装することが多いです。

これらのテクニックを適切に組み合わせることで、APIリクエストの重複実行を効果的に抑制し、サーバーへの不要な負荷やクライアント側のパフォーマンス低下を防ぐことができます。(参考情報:useRefの活用、AbortControllerの利用、キャッシュ機構の導入)

4.3. React Developer Toolsを活用したデバッグ手法

Reactアプリケーションのデバッグには、ブラウザ拡張機能として提供されているReact Developer Toolsが非常に強力です。特に、コンポーネントのレンダリング状況やuseEffectの実行タイミングを視覚的に把握するのに役立ちます。

このツールを使用すると、以下のようなデバッグ情報を得ることができます。

  • コンポーネントツリーの確認:どのコンポーネントがレンダリングされているか、その親子関係を視覚的に確認できます。
  • PropsとStateの編集・監視:選択したコンポーネントのPropsやStateの現在値をリアルタイムで確認・編集できます。これにより、値の変化がコンポーネントの挙動にどう影響するかを素早くテストできます。
  • レンダリングのハイライト:「Highlight updates when components render」機能を有効にすると、更新されたコンポーネントが一時的にハイライト表示されるため、どのコンポーネントが、いつ、なぜ再レンダリングされているかを一目で把握できます。これにより、意図しない再レンダリングの原因特定に繋がります。
  • プロファイラー:アプリケーションのパフォーマンスボトルネックを特定するために、レンダリングにかかる時間を測定し、最適化のヒントを得ることができます。

これらの機能を活用することで、useEffectが2回実行される現象やその他のパフォーマンス問題を効率的にデバッグし、解決に導くことができるでしょう。(参考情報:React Developer Tools)

よくあるエラー(404, 500, 502)との関連性

5.1. 2回実行が引き起こす間接的なAPIエラー(404, 500)

Reactコンポーネントの2回実行、特にuseEffectでの重複実行は、直接的に404 Not Foundや500 Internal Server ErrorといったHTTPエラーを引き起こすわけではありませんが、間接的にこれらのエラーにつながる可能性があります。

例えば、useEffect内でAPIリクエストを行っている場合、開発環境でStrict Modeによりリクエストが2回送信されることがあります。もしAPIエンドポイントが、特定の条件(例: 既に作成済みのリソースを再度作成しようとする)でエラーを返すように設計されている場合、2回目のリクエストが原因でサーバーエラー(例: 409 Conflictや、それを適切に処理できない場合の500 Internal Server Error)となる可能性があります。

また、存在しないリソースにアクセスしようとする404 Not Foundエラーは、リクエストのURLやパラメータがクライアント側で誤って構築された場合に発生しますが、2回目の実行時に特定の状態が不整合を起こし、無効なリクエストが生成されることで偶発的に発生することも考えられます。

これらの問題は、本番環境では発生しないStrict Modeの挙動によって開発中に顕在化するため、早期に検出してAPI呼び出しの堅牢性を高める良い機会となります。(この項目は参考情報からの推測と一般的な知識に基づいています)

5.2. サーバーサイドレンダリング(SSR)環境での注意点

Next.jsなどのフレームワークを用いたサーバーサイドレンダリング(SSR)環境では、Reactコンポーネントがサーバーとクライアントの両方でレンダリングされるため、useEffectの挙動にはさらに注意が必要です。

SSR環境では、初期レンダリングがサーバーで行われ、その後クライアント側で「ハイドレーション」と呼ばれるプロセスでReactアプリケーションが再起動します。このハイドレーションの過程でも、useEffectが実行される可能性があります。

特に、データフェッチを行うuseEffectは、SSR環境下で重複したAPIリクエストを引き起こすことがあります。サーバー側で一度データをフェッチしてHTMLに埋め込んだにもかかわらず、クライアント側で再度同じデータをフェッチしてしまうといったケースです。これにより、サーバーへの負荷が増加したり、パフォーマンスが低下したりする可能性があります。

もしサーバーが過負荷状態になったり、クライアントからの重複リクエストを適切に処理できなかったりすると、502 Bad Gateway(リバースプロキシとバックエンドサーバー間の通信エラー)のようなエラーが発生するリスクも高まります。SSRにおいては、データの二重フェッチを防ぐための適切なデータフェッチ戦略(例: SWR, React Queryなどを用いたキャッシュ)が不可欠です。(この項目は参考情報からの推測と一般的な知識に基づいています)

5.3. 効果的なエラーハンドリングと予防策

Reactの「2回実行」現象が引き起こしうる間接的なAPIエラーやSSR環境での問題を未然に防ぎ、効果的にエラーハンドリングを行うためには、いくつかの予防策を講じることが重要です。

  1. APIリクエストの冪等性:サーバーサイドのAPIは、同じリクエストが複数回送信されても同じ結果になるように冪等性を確保することが理想的です。特にデータ作成や更新APIでは、重複したリクエストが意図しないデータを生成しないように設計します。
  2. デバウンス・スロットル:短期間に何度もAPIリクエストが送られるのを防ぐため、ユーザー入力やイベント発生時のAPI呼び出しにデバウンスやスロットルを適用します。
  3. クライアントサイドでの適切なエラーハンドリング:APIから返されるエラーコード(404, 500など)をクライアントサイドで適切にキャッチし、ユーザーに分かりやすいメッセージを表示したり、ログを記録したりする仕組みを構築します。
  4. 開発中のStrict Mode活用useEffectのクリーンアップ関数を徹底的にテストし、APIリクエストのAbortController利用やイベントリスナーの解除を確実に行うことで、本番環境での潜在的な問題を早期に発見し、対処することができます。

これらの対策を講じることで、Reactの特殊な挙動が引き起こす可能性のある問題を最小限に抑え、安定したアプリケーションを提供できるようになります。(この項目は参考情報からの推測と一般的な知識に基づいています)

Reactの「2回呼ばれる・実行される」現象は、特に開発環境におけるStrict Modeの動作によるものです。これは、潜在的なバグを発見し、より堅牢なコンポーネントを作成するためのReactの洗練された仕組みなのです。根本的な対策としては、useEffectのクリーンアップ関数を適切に実装し、依存配列を正確に管理することが非常に重要です。APIリクエストなどの副作用を伴う処理では、AbortControllerやカスタムフック、キャッシュ機構などの工夫も有効です。この挙動は開発環境に限定されるため、本番環境への影響はありません。この理解を深め、より質の高いReactアプリケーション開発を目指しましょう。

本記事は、提供された「Reactの「2回呼ばれる・実行される」現象を徹底解説!原因と対策」に関する参考情報に基づいて作成されています。