How to Fix 'Module not found: Can't resolve react-dom/client' in Create React App with React 18
Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 5 mins
TL;DR
- What broke:
react-dom/clientis a submodule introduced inreact-dom@18. If your installedreact-domis17.xor lower — or there's a version mismatch betweenreactandreact-dom— the bundler can't resolve the path and the entire app fails to compile. - How to fix it: Upgrade both
reactandreact-domto^18.xand migrateindex.jsfrom the legacyReactDOM.render()API toReactDOM.createRoot(). - Fast path: Use our Client-Side Sandbox below to auto-refactor this — paste your
index.jsandpackage.jsonand get the corrected output instantly without leaking your code to a third-party server.
The Incident (What Does the Error Mean?)
You hit this at build time or npm start:
Module not found: Error: Can't resolve 'react-dom/client'
in /your-project/src/index.js
Immediate consequence: Webpack/CRA's module resolver walks node_modules/react-dom/ and finds no client.js entry point because you're running react-dom@17 or earlier. The entire dev server or production build halts. Nothing renders. Your CI pipeline fails on the first step.
This is not a transient error. It will not self-heal. Every subsequent npm start or npm run build will throw the same error until the version mismatch is corrected.
The Attack Vector / Blast Radius
This is a hard compile-time failure, not a runtime warning. The blast radius:
- Dev environment: Zero hot-reload, zero local testing possible.
- CI/CD pipeline: Build step exits non-zero. If your pipeline lacks proper gate checks, a misconfigured auto-merge or dependency bot (Dependabot, Renovate) could have silently downgraded
react-domwhile leavingreactat18.x, creating this split. - Production deploys: If this slips past a broken CI gate (e.g., tests were skipped), you ship a blank white screen to users. No error boundary catches a compile-time module resolution failure.
- Root trigger pattern: The most common cause is running
npm install react-domwithout pinning@18, pulling17.0.2from a legacy lockfile, or apackage.jsonwith"react-dom": "^17.0.0"that was never updated when React 18 was adopted.
How to Fix It (The Solution)
Basic Fix
Step 1: Align package versions.
npm install react@18 react-dom@18
or with Yarn:
yarn add react@18 react-dom@18
Step 2: Migrate src/index.js (or index.tsx) to the React 18 root API.
- import ReactDOM from 'react-dom';
+ import ReactDOM from 'react-dom/client';
import App from './App';
- ReactDOM.render(
- <React.StrictMode>
- <App />
- </React.StrictMode>,
- document.getElementById('root')
- );
+ const root = ReactDOM.createRoot(document.getElementById('root'));
+ root.render(
+ <React.StrictMode>
+ <App />
+ </React.StrictMode>
+ );
Step 3: Verify alignment.
npm ls react react-dom
Both must report 18.x.x. Any version mismatch here is the bug.
Enterprise Best Practice
Version drift across react and react-dom is a recurring problem in monorepos and teams using automated dependency updates. Lock it down at the tooling level:
1. Pin exact peer dependency enforcement in package.json:
"dependencies": {
- "react": "^17.0.0",
- "react-dom": "^17.0.0"
+ "react": "18.3.1",
+ "react-dom": "18.3.1"
}
Use exact versions (no ^ or ~) in production apps. Let your upgrade PRs be explicit and reviewed.
2. Add a peerDependencies check script:
+ "scripts": {
+ "check-peers": "npm ls react react-dom | grep -v deduped"
+ }
3. For monorepos (Nx, Turborepo): Hoist react and react-dom to the root package.json and add a resolutions block (Yarn) or overrides block (npm 8+) to prevent sub-packages from pulling divergent versions:
+ "overrides": {
+ "react": "18.3.1",
+ "react-dom": "18.3.1"
+ }
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
This class of error — silent dependency version drift — is 100% preventable with the right pipeline gates.
1. Enforce version parity with a pre-build lint step.
Add to your CI pipeline (GitHub Actions example):
- name: Validate React peer dependency alignment
run: |
REACT_VER=$(node -p "require('./node_modules/react/package.json').version")
REACTDOM_VER=$(node -p "require('./node_modules/react-dom/package.json').version")
if [ "$REACT_VER" != "$REACTDOM_VER" ]; then
echo "FATAL: react ($REACT_VER) and react-dom ($REACTDOM_VER) version mismatch."
exit 1
fi
2. Use npm ci instead of npm install in CI. npm ci installs strictly from package-lock.json and will error on lockfile/manifest divergence — it will never silently resolve a different version.
- run: npm install
+ run: npm ci
3. Configure Renovate or Dependabot to group React updates. Never let react and react-dom be updated in separate PRs:
// renovate.json
{
"packageRules": [
{
"matchPackageNames": ["react", "react-dom"],
"groupName": "React core"
}
]
}
4. Add depcheck or npm-check to your pre-commit hooks to catch unused or mismatched peer dependencies before they hit CI.