Initializing Enclave...

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/client is a submodule introduced in react-dom@18. If your installed react-dom is 17.x or lower — or there's a version mismatch between react and react-dom — the bundler can't resolve the path and the entire app fails to compile.
  • How to fix it: Upgrade both react and react-dom to ^18.x and migrate index.js from the legacy ReactDOM.render() API to ReactDOM.createRoot().
  • Fast path: Use our Client-Side Sandbox below to auto-refactor this — paste your index.js and package.json and 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-dom while leaving react at 18.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-dom without pinning @18, pulling 17.0.2 from a legacy lockfile, or a package.json with "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.

Related Diagnostics

"Part of the Syntax Utility Matrix."

View all 153 Syntax Tools →