現象の確認
Reactアプリを開いたとき — 開発モードでも本番ビルドでも — 完全に真っ白なページが表示される。エラーも出ない。スピナーも出ない。何もない。ブラウザのコンソールはきれいなままか、あるいはミニファイされたスタックトレースの奥深くに謎めいたJavaScriptエラーが1つ埋もれているだけかもしれない。
厄介なのは、Reactが明確なエラーを何も投げずに、サイレントにマウント失敗することがある点だ。ルートの <div id="root"> はただそこに空っぽのまま座っている。
まず素早く診断する
いきなりコードを変更するのはやめよう。DevToolsを開いて、次の3つを確認する:
- Consoleタブ — 普段は無視するような警告も含め、JSエラーを探す
- Networkタブ — JSバンドルが200ステータスで正常に読み込まれているか、それとも404になっているか
- Elementsタブ —
#rootを確認する。完全に空なら、Reactは一度もマウントされていない
ここで見つかった内容が、以下のどの修正が自分に当てはまるかを決める。
根本原因と修正方法
1. homepage または base パスの誤り(本番環境で最も多い原因)
https://example.com/myapp/ のようなサブディレクトリにデプロイしているのに、アプリがルート(/)に置かれているかのようにビルドしている場合、ブラウザは index.html は問題なく読み込むが、その後すべてのJSおよびCSSバンドルを取得しようとしてサイレントに404になる。Reactは実行される機会すら得られない。
Networkタブの確認: JSでフィルタリングすると、.chunk.js ファイルが200ではなく404を返しているのがわかる。
Create React Appの修正 — package.json に追加する:
{
"homepage": "https://example.com/myapp"
}
Viteの修正 — vite.config.ts に設定する:
export default defineConfig({
base: '/myapp/',
})
React Routerの修正 — BrowserRouter を使っている場合は、basename も合わせて指定する:
import { BrowserRouter } from 'react-router-dom';
<BrowserRouter basename="/myapp">
<App />
</BrowserRouter>
再ビルドして再デプロイしよう。Networkタブですべてのアセットが200を返すようになるはずだ。
2. レンダリング中にJavaScriptエラーが発生(サイレントにキャッチされる)
React 16以降は、Error Boundaryが設置されていない場合、レンダーエラーをキャッチしてコンポーネントツリー全体をアンマウントする。開発モードでは大きな赤いオーバーレイが表示されるが、本番環境では? 真っ白なページと完全な沈黙だ。
Error Boundaryを追加すれば、何がクラッシュしているのかがついにわかるようになる:
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, info) {
console.error('Render error caught:', error, info);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong: {this.state.error?.message}</h1>;
}
return this.props.children;
}
}
export default ErrorBoundary;
アプリのルートを囲む:
// index.tsx または main.tsx
import ErrorBoundary from './ErrorBoundary';
ReactDOM.createRoot(document.getElementById('root')!).render(
<ErrorBoundary>
<App />
</ErrorBoundary>
);
次回レンダリング中にクラッシュが発生しても、真っ白なページの代わりにメッセージが表示されるようになる。
3. ルート要素のIDの不一致
よくあるサイレントキラーだ。index.html に <div id="app"> があるのに、エントリーファイルでは document.getElementById('root') を呼んでいる。Reactは null を受け取り、マウントを完全にスキップするが、何も警告しない。
// IDが一致しない場合、getElementById はnullを返す
const container = document.getElementById('root');
// 何もしないままサイレントに失敗するのではなく、早期にエラーを投げる:
if (!container) {
throw new Error('Root element #root not found in index.html');
}
ReactDOM.createRoot(container).render(<App />);
素早く確認する方法:Elementsタブを開いて id="root" を検索する — 大文字・小文字も含め、JS内の文字列と完全に一致していなければならない。
4. 環境変数の欠落または不正な形式
たとえばアプリがモジュールレベルで import.meta.env.VITE_API_URL を読み込んでいるとする。その変数が本番の .env ファイルに定義されていなければ、値は undefined になる。それを直ちに使おうとするコード — たとえばAxiosのベースURLの構築など — は最初のレンダリングでクラッシュする。
各ビルドターゲット用の .env ファイルが正しいか確認する:
# .env.production — CRAの場合
REACT_APP_API_URL=https://api.example.com
# .env.production — Viteの場合
VITE_API_URL=https://api.example.com
机に貼り付けておく価値のある3つのルール:
- CRAの変数は
REACT_APP_で始まらなければならない。そうでなければビルドから見えない - Viteの変数は
VITE_で始まらなければならない - モジュールのトップレベルで環境変数を読み込まないこと — nullチェックを追加できるコンポーネントや関数の中で使う
5. 循環インポートまたはモジュールインポートの失敗
循環依存があると、モジュールが実行時に undefined として解決される。そのモジュールから関数を呼び出そうとするコードは最初のレンダリングでクラッシュする — 本番環境ではサイレントに。
まず、ビルド自体がエラーを報告していないか確認する:
# CRA
npm run build 2>&1 | grep -i error
# Vite
npx vite build 2>&1 | grep -i error
循環インポートを直接見つけるには、madge が最良の友だ:
npx madge --circular --extensions ts,tsx src/
共有の型やユーティリティをインポートループの外側に置く別ファイルに移動することで循環を断ち切る — サイクル内のものはそこからインポートしない。
6. Reactのバージョン競合
1つのバンドルに2つのバージョンのReactが存在すると、互いに知らない2つの別々のフックシステムができてしまう。フックが壊れ、アプリはレンダリングされない。これは通常、サードパーティライブラリがReactをピア依存として扱わず、独自のReactのコピーをバンドルしているときに起きる。
npm ls react
# または
pnpm why react
2つの異なるバージョンを見つけたら? package.json で両方を固定する:
"resolutions": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
npm install --force を実行し、再ビルドして、再度 npm ls react を確認する — ツリー全体で単一バージョンが表示されるはずだ。
予防策
- アプリのルートにError Boundaryを追加する — 本番環境では必須だ。サイレントな失敗は大きな失敗であるべきだ。
- リリース前にローカルで本番ビルドをテストする:
npm run build && npx serve -s build(CRA)またはnpx vite preview(Vite) - 最初から
base/homepageを設定する — アプリがサブディレクトリに置かれることがわかっているなら、後から修正すると必ず予期しない問題が起きる - 起動時に必須の環境変数を検証する — 10秒後に真っ白な画面ではなく、即座に明確なエラーを投げるようにする
- ステージング環境でソースマップを有効にする — ソースマップなしでミニファイされた本番エラーをデバッグするのは、通りの名前がない地図を読むようなものだ
確認チェックリスト
- [ ] Networkタブ:すべてのJS/CSSバンドルが200を返し、404になっているものがないこと
- [ ] Elementsタブ:ページ読み込み後、
#rootに子要素があること - [ ] Console:未キャッチのエラーや未処理のPromise拒否がないこと
- [ ] Error Boundary:クラッシュ時に真っ白になる代わりにフォールバックUIが表示されること
- [ ] ローカルの本番プレビューが通過すること:デプロイ前に
npx serve -s buildが動作すること

