何が起きているか
Reactアプリが起動した直後、コンソールに次のエラーが表示されてクラッシュします:
Error: Target container is not a DOM element.
ReactがマウントポイントとなるDOM要素を見つけられませんでした。原因として、<div id="root">が最初から存在しなかった、編集中に削除してしまった、あるいはよりやっかいなケースとして、ブラウザがその要素のパースを終える前にスクリプトが実行されてしまったことが考えられます。Create React AppからViteへの移行はよくあるトリガーで、両バンドラーはindex.htmlの置き場所が異なるためです。
デバッグの手順
ステップ1:ReactDOMが実際に受け取っている値を確認する
何かを変更する前に、エントリーファイルでコンテナをログ出力してみましょう:
// React 18
const container = document.getElementById('root');
console.log(container); // null = 要素が存在しない
ReactDOM.createRoot(container).render(<App />);
// React 16/17
console.log(document.getElementById('root'));
ReactDOM.render(<App />, document.getElementById('root'));
コンソールにnullが表示されましたか?それが答えです。マウント時点でその要素が存在しないということです。あとはその原因を突き止めるだけです。
ステップ2:index.htmlを確認する
public/index.html(CRA)またはプロジェクトルートにあるindex.html(Vite)を開きます。確認すべき行はこちらです:
<div id="root"></div>
よく引っかかる落とし穴が3つあります:
- 編集中にdivが削除されてしまった — まずこれを確認してください
idにタイポや大文字・小文字の誤りがある(id="Root"とid="root"の違いなど)- HTMLでは
id="app"を使っているのに、JSではgetElementById('root')を呼んでいる — またはその逆
ステップ3:手書きHTMLのscriptタグの位置を確認する
バンドラーによるスクリプト注入を使っていない場合、ブラウザがページのパースを終える前にスクリプトが実行されてしまい、divが存在していてもgetElementByIdがnullを返すことがあります。
解決策
修正1:index.htmlにルートdivを追加する
十中八九、これが原因です。編集中にdivが消えてしまったのです。HTMLに正しいIDのコンテナが存在することを確認してください:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>My React App</title>
</head>
<body>
<div id="root"></div>
<!-- バンドラーがここにスクリプトバンドルを注入する -->
</body>
</html>
次に、エントリーファイルのIDが完全に一致していることを確認します:
// src/main.jsx (Vite) または src/index.js (CRA)
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const container = document.getElementById('root'); // HTMLのidと一致させること
ReactDOM.createRoot(container).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
修正2:scriptタグをルートdivの後に移動する
<head>内のスクリプトはbodyのパース前に実行されます。スクリプトを手動で読み込んでいる場合、ブラウザが<div id="root">の行を読む前にJSが実行されてしまいます。
誤った例:
<head>
<script src="bundle.js"></script> <!-- #rootが存在する前に実行される -->
</head>
<body>
<div id="root"></div>
</body>
正しい例:
<body>
<div id="root"></div>
<script src="bundle.js"></script> <!-- #rootがDOMに存在した後に実行される -->
</body>
スクリプトを<head>内に置きたい場合はdeferを追加してください。これにより、ブラウザはスクリプトを並行してダウンロードしつつ、HTMLパース完了後にのみ実行するようになります:
<head>
<script src="bundle.js" defer></script>
</head>
修正3:マウント時にnullガードを追加する
CMSが挿入するページ、ブラウザ拡張機能、マイクロフロントエンド構成では、要素の存在が保証されないことがあります。明示的にガード処理を追加しましょう:
const container = document.getElementById('root');
if (!container) {
throw new Error(
'Root element not found. Make sure index.html contains <div id="root"></div>'
);
}
ReactDOM.createRoot(container).render(<App />);
不足している要素を具体的に示すカスタムエラーは、Reactの汎用的なDOMエラーよりも常に役立ちます。
修正4:バンドラーに合わせたindex.htmlの置き場所を確認する
ViteとCRAではindex.htmlの置き場所が異なります。移行作業の途中で多くの人がこれに引っかかります:
# Viteプロジェクトの構成
my-vite-app/
├── index.html <-- ここに置く必要あり(<div id="root">を含む)
├── src/
│ └── main.jsx
└── vite.config.js
# CRAプロジェクトの構成
my-cra-app/
├── public/
│ └── index.html <-- ここに置く必要あり
├── src/
│ └── index.js
└── package.json
index.htmlを間違ったフォルダに置くと、バンドラーがファイルを見つけられないか、ルートdivのない空白ページが表示されます。どちらの場合も、有用なエラーメッセージは出ません。
修正5:マウントポイントをプログラムで作成する
ReactをReact以外のページ(ウィジェットやサードパーティプラグインなど)に組み込む場合、スクリプト実行時にマウント要素が存在しないことがあります。要素の存在を前提とせず、自分で作成しましょう:
// 脆弱な方法:要素がすでに存在することを前提としている
const existingEl = document.getElementById('my-widget');
ReactDOM.createRoot(existingEl).render(<Widget />); // elがnullの場合に失敗する
// 確実な方法:動的に作成する
const mountPoint = document.createElement('div');
mountPoint.id = 'my-widget';
document.body.appendChild(mountPoint);
ReactDOM.createRoot(mountPoint).render(<Widget />);
修正を確認する
開発サーバーを再起動してください。コンソールにReactのエラーが表示されなければ、修正完了のサインです。DevToolsで以下を実行してダブルチェックしましょう:
// ブラウザのDevToolsコンソール:
document.getElementById('root'); // nullではなく要素が返るはず
要素が取得できましたか?Reactはマウントできます。それでもまだ空白ページが表示される場合は、別のJSランタイムエラーが発生しています。マウントの問題は解決されましたが、他の箇所で何か壊れています。
開発中にすばやくシグナルを得るためのワンライナーアサーションです:
const container = document.getElementById('root');
console.assert(container !== null, '#root element missing from index.html');
ReactDOM.createRoot(container).render(<App />);
学んだこと
- JavaScriptの文字列比較は大文字・小文字を区別します。
"Root"と"root"は同じIDではありません。片方は正常に動作し、もう片方ではReactがサイレントに失敗します。 - バンドラー(Vite、CRA、webpack)は自動的に
<body>の末尾にスクリプトを注入します。手書きのHTMLにはその処理がないため、スクリプトの配置は自分で管理する必要があります。 - バンドラーを移行するたびに
index.htmlが誤ったフォルダに置かれるリスクがあります。CRA→Vite移行後に置き場所を確認する30秒の作業が、1時間の原因究明を防いでくれます。 - マウント時に分かりやすいメッセージを持つnullガードを追加することは、将来の自分への贈り物です。「Root element not found」は「Target container is not a DOM element」より何百倍も対処しやすいエラーメッセージです。

