What You're Seeing
You open your React app โ dev mode or a fresh production build โ and get a completely blank white page. No error. No spinner. Nothing. The browser console might be clean, or it might have one cryptic JavaScript error buried three levels deep in a minified stack trace.
Here's the frustrating part: React can silently fail to mount without throwing anything obvious. The root <div id="root"> just sits there, empty.
Quick Diagnosis First
Don't start changing code yet. Open DevTools and check these three things:
- Console tab โ look for JS errors, even warnings you'd normally ignore
- Network tab โ did your JS bundle actually load with a 200 status, or is it 404ing?
- Elements tab โ inspect
#root; completely empty means React never mounted at all
What you find here determines which fix below actually applies to you.
Root Causes and Fixes
1. Wrong homepage or base Path (Most Common in Production)
Deploying to a subdirectory like https://example.com/myapp/ but building as if the app lives at root (/)? The browser loads index.html just fine, then silently 404s every JS and CSS bundle it tries to fetch. React never gets a chance to run.
Network tab check: filter by JS โ you'll see your .chunk.js files returning 404 instead of 200.
Fix for Create React App โ add to package.json:
{
"homepage": "https://example.com/myapp"
}
Fix for Vite โ set in vite.config.ts:
export default defineConfig({
base: '/myapp/',
})
Fix for React Router โ if you're using BrowserRouter, wire in basename too:
import { BrowserRouter } from 'react-router-dom';
<BrowserRouter basename="/myapp">
<App />
</BrowserRouter>
Rebuild and redeploy. The Network tab should now show all assets hitting 200.
2. JavaScript Error Thrown During Render (Caught Silently)
React 16+ catches render errors and unmounts the entire component tree when there's no Error Boundary in place. In development you get a big red overlay. In production? A blank page and complete silence.
Add an Error Boundary and you'll finally see what's crashing:
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;
Wrap it around your app root:
// index.tsx or main.tsx
import ErrorBoundary from './ErrorBoundary';
ReactDOM.createRoot(document.getElementById('root')!).render(
<ErrorBoundary>
<App />
</ErrorBoundary>
);
Next time something crashes during render, you'll see a message instead of a blank page.
3. Root Element ID Mismatch
Classic silent killer. Your index.html has <div id="app"> but your entry file calls document.getElementById('root'). React gets null, skips mounting entirely, and doesn't complain about it.
// getElementById returns null if the ID doesn't match
const container = document.getElementById('root');
// Throw early instead of silently doing nothing:
if (!container) {
throw new Error('Root element #root not found in index.html');
}
ReactDOM.createRoot(container).render(<App />);
Quick check: open the Elements tab and search for id="root" โ it must match the string in your JS exactly, including casing.
4. Missing or Malformed Environment Variables
Say your app reads import.meta.env.VITE_API_URL at the module level. If that variable isn't defined in your production .env file, the value is undefined. Any code that immediately tries to use it โ say, constructing an Axios base URL โ throws on the first render.
Check your .env files are correct for each build target:
# .env.production โ for CRA
REACT_APP_API_URL=https://api.example.com
# .env.production โ for Vite
VITE_API_URL=https://api.example.com
Three rules worth tattooing on your desk:
- CRA variables must start with
REACT_APP_or they're invisible to the build - Vite variables must start with
VITE_ - Don't read env vars at the module's top level โ keep them inside components or functions where you can add a null check
5. Circular Imports or Failed Module Imports
A circular dependency causes a module to resolve as undefined at runtime. Whatever tries to call a function from that module crashes on the first render โ silently, in production.
First, check whether the build itself is complaining:
# CRA
npm run build 2>&1 | grep -i error
# Vite
npx vite build 2>&1 | grep -i error
To find circular imports directly, madge is your best friend:
npx madge --circular --extensions ts,tsx src/
Break the cycle by pulling shared types or utilities into a separate file that sits outside the import loop โ nothing in the cycle imports from it.
6. React Version Conflicts
Two versions of React in one bundle means two separate hook systems that don't know about each other. Hooks break. The app doesn't render. This usually happens when a third-party library bundles its own copy of React instead of treating it as a peer dependency.
npm ls react
# or
pnpm why react
Spot two different versions? Pin both in package.json:
"resolutions": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
Run npm install --force, rebuild, and check npm ls react again โ you should see a single version across the whole tree.
Prevention
- Add an Error Boundary at the app root โ non-negotiable for production. Silent failures should be loud failures.
- Test production builds locally before shipping:
npm run build && npx serve -s build(CRA) ornpx vite preview(Vite) - Set
base/homepagefrom day one if you know the app will live in a subdirectory โ retrofitting it later always causes surprises - Validate required env vars at startup so the app throws a clear error immediately, not a blank screen 10 seconds later
- Enable source maps in staging โ debugging minified production errors without them is like reading a map with no street names
Verification Checklist
- [ ] Network tab: all JS/CSS bundles return 200, none are 404ing
- [ ] Elements tab:
#roothas child elements after page load - [ ] Console: no uncaught errors or unhandled promise rejections
- [ ] Error Boundary: renders fallback UI instead of going blank when something crashes
- [ ] Local production preview passes:
npx serve -s buildworks before you deploy

