問題:Reactが気づかないうちにDOMが変更されるReactアプリは開発環境では完璧に動作します。しかし本番環境では、ユーザーからアプリが完全にクラッシュする——いわゆる白い画面が表示される——という報告が寄せられます。ログを確認するとNotFoundErrorが見つかります。これは、Reactが内部で保持しているページのマップが、ブラウザが実際に保持しているHTMLと一致しなくなったために発生します。
Reactは自分が管理するDOMノードを完全に制御していると想定しています。ブラウザ拡張機能や自動翻訳ツールなどの外部の力が要素を移動・削除・ラップすると、Reactはその要素を追跡できなくなります。最終的にその要素をアンマウントしようとしたとき、親子関係が切断されているためブラウザはエラーをスローします。
よくある原因- **Google Translate:**これが最大の原因です。翻訳を適用するためにテキストノードを<font>タグに置き換えることが多く、Reactが監視していたノードを実質的に削除してしまいます。- **Grammarlyと広告ブロッカー:**これらのツールはテキストのハイライトやポップアップ表示のために<div>や<span>要素をUIに頻繁に注入します。- **レガシーjQueryプラグイン:**要素を手動で移動させる古いスライダーやツールチップライブラリを使用している場合、Reactは最終的にもう存在しない子要素を「削除」しようとします。## Reactがクラッシュする理由ReactはVirtual DOMを使用してUIを更新する最も効率的な方法を計算します。コンポーネントを非表示にするような状態を切り替えると、Reactは実際のDOMに対してremoveChildコマンドを実行します。
ブラウザは厳格です。ReactがparentNode.removeChild(child)を呼び出したとき、翻訳ツールがそのchildを別のコンテナに移動させていた場合、ブラウザはリクエストを拒否します。これは単なる警告ではなく、JavaScriptの実行スレッドを停止させる致命的なエラーです。
解決策1:「テキストノード」ガードremoveChildによるクラッシュの多くは動的テキストが原因です。Reactは文字列を生のテキストノードとしてレンダリングすることがよくあります。Google Translateがそのテキストを<font>タグでラップすると、元のテキストノードへのReactの参照が「死んだ」参照になってしまいます。
これを修正するには、動的な文字列を裸のまま放置しないでください。<span>のような安定した要素でラップしましょう。Reactは浮遊するテキストよりも永続的なHTML要素のほうをはるかに追跡しやすいのです。
// ❌ 危険:Google Translateがこれを壊す
{user.isLoggedIn && `おかえりなさい、${user.name}`}
// ✅ 安全:Reactは生のテキストではなくspanを追跡する
{user.isLoggedIn && <span>おかえりなさい、{user.name}</span>}
解決策2:「ブラックボックス」パターンを使うD3.jsやレガシーjQueryウィジェットのようなDOMを操作するライブラリを使用する必要がある場合、Reactからそれを隠す必要があります。「ブラックボックス」コンポーネントを作成しましょう。これはReactが外側のシェルを一度だけレンダリングしてから脇に退き、サードパーティライブラリが手動で作業を行えるようにするコンテナです。
import { useEffect, useRef } from 'react';
function MapWidget({ coordinates }) {
const elRef = useRef(null);
useEffect(() => {
// refでライブラリを初期化する
const map = window.ExternalMapLib.init(elRef.current);
map.setPosition(coordinates);
// 重要:ゴーストノードを防ぐために手動でクリーンアップする
return () => map.destroy();
}, [coordinates]);
// ReactはこのdivのみをManageする。内部には触れない。
return <div ref={elRef} />;
}
解決策3:React 18のStrict Modeへの対応React 18では、StrictModeが開発環境でコンポーネントを2回マウントします。これはクリーンアップのバグを発見するために設計されています。useEffectがDOM要素を作成するにもかかわらず、クリーンアップ関数がそれを削除できない場合、IDの重複やノードの不一致が発生します。これはホットリローディング中にremoveChildエラーが発生するよくある原因です。
クリーンアップロジックを常に検証してください。document.bodyに何かを追加した場合、コンポーネントのアンマウント時に必ず削除する必要があります。
useEffect(() => {
const portalNode = document.createElement('div');
document.body.appendChild(portalNode);
return () => {
if (document.body.contains(portalNode)) {
document.body.removeChild(portalNode);
}
};
}, []);
修正を確認する方法エラーが解消されたと思い込まないでください。クラッシュを引き起こす状況をシミュレートできます。
- **翻訳機能をテストする:**Chromeでアプリを開いてください。任意の場所を右クリックして翻訳機能を選択してください。以前クラッシュを引き起こしたUI要素を切り替えてみましょう。- **「ゴースト」要素を確認する:**DevToolsの「Elements」タブを使用します。
<font>タグやGrammarlyなどの拡張機能によって追加された要素を探してください。それらがReactツリーの内部にある場合、該当セクションを<span>タグでラップしてください。- **本番ロギング:**このエラーはローカル開発環境では捕捉しにくいため、SentryやLogRocketのようなツールを使用して実際のユーザーからのスタックトレースを収集してください。

