Minified React Error #185の解決:本番環境での無限ループを停止させる

intermediate⚛️ React2026-06-30| モダンブラウザ上で動作する React (v16, v17, v18) の本番ビルド。

Error Message

Error: Minified React error #185; visit https://reactjs.org/docs/error-decoder.html?invariant=185 for the full message.
#react#本番環境#デバッグ#javascript

深夜2時の本番環境アラート

夜も更けた頃、エラー監視ツールのグラフが急上昇しました。ユーザーからは、ダッシュボードがフリーズしている、あるいは最悪の場合、真っ白になっているという報告が寄せられています。本番環境のコンソールを開き、明確なスタックトレースを期待しますが、表示されるのはもどかしいほど曖昧なリンクだけです。分析すると以下のようになっています:

Error: Minified React error #185; visit https://reactjs.org/docs/error-decoder.html?invariant=185 for the full message.

Reactは、貴重なキロバイト数を節約するために、本番環境では詳細なエラーメッセージを削除します。そのリンクを辿ると、デコーダーは真実を明らかにします:"Maximum update depth exceeded"(最大更新深度を超えました)。これは、アプリが無限ループに陥っていることを意味します。Reactには、ブラウザがフリーズするのを防ぐため、更新のネストが一定の制限(通常は50回)に達するとコンポーネントツリーを強制終了する安全装置が備わっています。

なぜループが発生するのか

本質的に、この問題はロジックのフィードバックループです。1つのレンダリングがState(状態)の変更をトリガーし、それが再びレンダリングを強制し、さらに同じState変更を再びトリガーします。これは非常に高速に発生し、多くの場合、1秒足らずで50回のレンダリング制限に達します。

開発者が陥りやすい主な原因は以下の通りです:

  • Stateの直接注入: 関数コンポーネントのボディ内で直接 setState を呼び出している。
  • 依存関係の罠: useEffect フックが、自身が監視しているStateを更新している。
  • 即時実行: onClick={handleClick()} ではなく onClick={handleClick} と記述すべき箇所で、実行形式で渡している。これにより、クリックを待つのではなく、レンダリングフェーズ中に関数が実行されてしまいます。
  • PropとStateの同期: クラスコンポーネントにおける getDerivedStateFromProps や同様のロジックにより、サイクルごとに更新がトリガーされている。

迅速な解決策:原因の特定

難読化(minify)されたコードのデバッグは悪夢です。SentryやDatadogのプライベートインスタンスに Source Maps をアップロードしている場合は、それを使用してください。不可解な at abc (main.js:10) を、UserProfile.tsx の実際の行に対応付けてくれます。

Source Mapsがない場合は、スタックトレースを頼りに推測する必要があります。XyZa、あるいは再び Xy といった、難読化された関数名の繰り返しのパターンを探してください。この繰り返しは、通常、ループしているコンポーネントを指し示しています。直近のGitコミット、特にデータ取得や複雑な useEffect ロジックに関連する変更を確認してください。

恒久的な解決策:連鎖を断ち切る

1. useEffectの依存関係を整理する

#185エラーのほとんどはフックに起因します。よくある間違いを見てみましょう:

useEffect(() => {
  setCount(count + 1);
}, [count]); // これは即座に無限ループを引き起こします。

変更対象の変数を監視する代わりに、関数型の更新(functional update)を使用します。これにより、依存関係配列に変数を追加する必要がなくなります:

useEffect(() => {
  // マウント時に一度だけ実行
  setCount(prev => prev + 1);
}, []); 

2. レンダリング時のハンドラ実行を止める

JSXに余計な括弧がないか確認してください。もし <button onClick={doSomething()}> となっていれば、それが原因です。その関数はボタンがレンダリングされた瞬間に実行されます。もし doSomething がStateを更新すれば、コンポーネントは再レンダリングされ、再び関数が実行され、アプリがクラッシュします。常に参照を渡すようにしてください: <button onClick={doSomething}>

3. 派生状態(Derived State)への移行

2つのStateを同期させるために useEffect を使うのはやめましょう。非効率で危険です。検索ワードに基づいてリストをフィルタリングする場合、filteredList をStateに保存してはいけません。代わりに useMemo を使用します:

// useEffectで2回目のレンダリングをトリガーする代わりに
const filteredData = useMemo(() => {
  return data.filter(item => item.active);
}, [data]);

これにより、初期レンダリング中に値が計算されるため、ライフサイクルのステップを1つ節約でき、ループのリスクを排除できます。

検証ステップ

修正をいきなり本番環境にプッシュして、うまくいくことを祈るだけではいけません。まずはステージング環境で修正を検証してください:

  • Profilerテスト: React DevToolsを開き、「Profiler」タブを使用します。数秒間のアクティビティを記録してください。単一のコンポーネントが1秒間に数十回もコミットしている場合、ループはまだ解消されていません。
  • CPUモニタリング: ブラウザのタスクマネージャーを確認してください。特定のページに移動したときにタブのCPU使用率が100%に跳ね上がり、そのまま維持される場合は、ロジックがまだループしています。

デバッグのプロの技

時として、ループは不適切な形式のBase64文字列や予期しないクエリパラメータなど、URL内の異常なデータによって引き起こされることがあります。深夜2時にURL主導のStateがなぜクラッシュするのか分からず行き詰まったときは、ToolCraftの URL Encoder/Decoder を使用しています。これにより、本番環境のURLスニペットを素早くパースして調査し、隠れた文字がパースロジックを失敗させ、更新ループをトリガーしていないか確認できます。

最後に、ツールを活用しましょう。CI/CDパイプラインで eslint-plugin-react-hooks を "error" に設定してください。これにより、コードが手元のPCを離れる前に、依存関係に関連するほぼすべてのループをキャッチできます。

Related Error Notes