Initializing Enclave...

How to Fix React-DOM Version Mismatch Errors Crashing npm start

Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 5–15 mins

TL;DR

  • What broke: react and react-dom are pinned to different major or minor versions in your package.json or a transitive dependency is pulling in a conflicting peer, causing the React runtime to refuse initialization.
  • How to fix it: Align react and react-dom to the same explicit version, audit peer dependency requirements of third-party packages, and lock the tree with package-lock.json or yarn.lock.
  • Shortcut: Use our Client-Side Sandbox below to auto-refactor this — paste your package.json and get a corrected diff without sending your config to a third-party server.

The Incident (What Does the Error Mean?)

You run npm start and the process dies immediately. The raw terminal output looks like one of these:

ERROR in ./node_modules/react-dom/cjs/react-dom.development.js
Module not found: Error: Can't resolve 'react' in '.../node_modules/react-dom'

// OR the more explicit peer conflict:
npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR!
npm ERR! While resolving: [email protected]
npm ERR! Found: [email protected]
npm ERR! node_modules/react
npm ERR!   react@"^18.2.0" from the root project
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer react@"^17.0.0" from [email protected]
npm ERR! node_modules/react-dom
npm ERR!   react-dom@"^17.0.2" from the root project

// OR at runtime after webpack compiles:
Warning: Invalid hook call. Hooks can only be called inside of a function component.
Uncaught Error: Cannot read properties of null (reading 'useState')

Immediate consequence: The dev server never starts. In CI, the build job fails at the npm install or npm run build step, blocking every PR merge. If this slips into a production Docker image built with --legacy-peer-deps, you get a broken runtime that throws cryptic hook errors instead of a clean install failure.


The Attack Vector / Blast Radius

This is not a security exploit in the classical sense — but the blast radius is wide:

  1. Duplicate React instances in the bundle. When webpack resolves two different react-dom versions, it may bundle both React runtimes. React's internal singleton state (__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED) gets split across instances. Every hook call (useState, useEffect, useContext) throws or silently corrupts state. This is a silent runtime bomb — tests may pass while the app is broken for end users.

  2. Transitive dependency cascade. A single UI library (e.g., @mui/[email protected] requiring react@^18, while your project has react-dom@17) can detonate the entire dependency tree. Running npm install with --legacy-peer-deps papers over the conflict but ships broken code.

  3. CI/CD pipeline paralysis. ERESOLVE errors are hard failures. Every engineer's branch, every preview deployment, and every staging build is blocked until the root package.json is corrected.

  4. Lock file drift. If package-lock.json was committed while --legacy-peer-deps was active, the conflict is invisible locally but explodes in clean CI environments that run strict npm ci.


How to Fix It (The Solution)

Step 1 — Diagnose the exact conflict

# Show the full peer dependency tree and flag conflicts
npm ls react
npm ls react-dom

# For npm 7+, get a clean conflict report
npm install --dry-run

Identify: (a) what version of react is in root, (b) what version of react-dom is in root, (c) which third-party package is pulling a mismatched peer.


Basic Fix — Align react and react-dom versions

// package.json
  "dependencies": {
-   "react": "^18.2.0",
-   "react-dom": "^17.0.2",
+   "react": "^18.2.0",
+   "react-dom": "^18.2.0",
    "react-scripts": "5.0.1"
  }

After editing:

# Delete the corrupted lock file and node_modules
rm -rf node_modules package-lock.json
npm install
npm start

Enterprise Best Practice — Pin exact versions, use overrides, enforce in CI

For monorepos or projects with stubborn transitive conflicts, use npm overrides (npm 8.3+) or yarn resolutions to force a single React version across the entire tree.

// package.json
  "dependencies": {
-   "react": "^18.2.0",
-   "react-dom": "^17.0.2",
+   "react": "18.2.0",
+   "react-dom": "18.2.0",
    "react-scripts": "5.0.1"
  },
+ "overrides": {
+   "react": "18.2.0",
+   "react-dom": "18.2.0"
+ }

For Yarn Berry (v2+):

// package.json
+ "resolutions": {
+   "react": "18.2.0",
+   "react-dom": "18.2.0"
+ }

For pnpm:

// package.json
+ "pnpm": {
+   "overrides": {
+     "react": "18.2.0",
+     "react-dom": "18.2.0"
+   }
+ }

After applying overrides:

rm -rf node_modules package-lock.json
npm install
# Verify no duplicate React instances exist
npm ls react | grep -v deduped
# Should return ONLY one version line

💡 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

1. Enforce peer dependency validation in your pipeline

Add this as a required CI step before the build step:

# .github/workflows/ci.yml
- name: Validate dependency tree
  run: |
    npm install --dry-run 2>&1 | tee /tmp/npm-dry-run.log
    if grep -q 'ERESOLVE\|peer dep' /tmp/npm-dry-run.log; then
      echo "::error::Peer dependency conflict detected. Fix package.json before merging."
      exit 1
    fi

2. Use npm ci — never npm install — in CI

# Dockerfile or CI script
- RUN npm install
+ RUN npm ci --ignore-scripts

npm ci requires a valid package-lock.json and fails on any tree inconsistency. It will catch this class of error on every build.

3. Add a check-peer-deps pre-commit hook

# Install the tool
npm install --save-dev check-peer-dependencies

# package.json scripts
"scripts": {
  "check-peers": "check-peer-dependencies"
}

Wire it into Husky:

// .husky/pre-push
npm run check-peers

4. Renovate Bot / Dependabot with grouped React updates

Configure Renovate to always update react and react-dom together in a single PR, preventing them from drifting:

// renovate.json
{
  "packageRules": [
    {
      "matchPackageNames": ["react", "react-dom", "@types/react", "@types/react-dom"],
      "groupName": "React core",
      "groupSlug": "react-core"
    }
  ]
}

5. Add a Webpack bundle duplicate check (runtime safety net)

npm install --save-dev duplicate-package-checker-webpack-plugin
// webpack.config.js or craco.config.js
const DuplicatePackageCheckerPlugin = require('duplicate-package-checker-webpack-plugin');

module.exports = {
  plugins: [
    new DuplicatePackageCheckerPlugin({ strict: true })
  ]
};

This fails the webpack build if duplicate react instances are detected in the bundle — your last line of defense before a broken artifact ships.

Related Diagnostics

"Part of the Syntax Utility Matrix."

View all 153 Syntax Tools →