Reactで発生する「Failed to execute removeChild on Node」エラーの修正方法

intermediate⚛️ React2026-07-10| React(v16、v17、v18)、Chrome、Safari、Edge。Google Translate、Grammarly、パスワードマネージャーなどによってトリガーされることが多い。

Error Message

NotFoundError: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.
#react#javascript#dom#デバッグ#web開発

問題: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のようなツールを使用して実際のユーザーからのスタックトレースを収集してください。

Related Error Notes