Upgrading to React 19 offers significant performance improvements, including the new React Compiler and streamlined Server Actions. However, early adopters often hit a wall immediately after running npm install.
Instead of a clean build, the terminal floods with ERESOLVE unable to resolve dependency tree.
This error occurs because the ecosystem takes time to catch up. Major libraries like Material UI (MUI), TanStack Query, or Testing Library often have peerDependencies pinned to React 17 or 18. When you attempt to force React 19 into the project, npm blocks the installation to prevent potential instability.
This guide details exactly why this happens and provides a surgical, production-grade method to resolve it using overrides—without downgrading React or relying permanently on legacy flags.
The Root Cause: Why NPM Blocks the Install
To fix the problem effectively, you must understand the mechanism triggering the error. Since version 7, npm automatically installs peer dependencies.
The Conflict Logic
When you declare a dependency in package.json, library authors define which versions of React their library is compatible with via peerDependencies.
For example, a library might specify:
"peerDependencies": {
"react": "^18.0.0"
}
This SemVer (Semantic Versioning) rule tells npm: "I only work with React versions greater than or equal to 18.0.0 and less than 19.0.0."
When you install React 19 (19.0.0), npm detects a strict violation. It realizes that resolving the tree results in two different major versions of React attempting to exist in the same context, or one package receiving a version it explicitly claims not to support.
To protect the integrity of your dependency tree, npm throws ERESOLVE.
The Quick (But Dirty) Fix vs. The Engineer's Fix
Most StackOverflow answers suggest appending flags to your install command. It is important to distinguish between a temporary patch and a permanent configuration.
The Flag Approach (Temporary)
You can bypass the check during a single installation using:
npm install --legacy-peer-deps
Why avoid this in production? This flag tells npm to ignore the peer dependency algorithm entirely. While it installs the packages, it leaves your package.json describing an invalid state. Future developers (or CI/CD pipelines) running a standard npm install will encounter the same crash unless they also know to add the flag. It creates "tribal knowledge" rather than configuration-as-code.
The Professional Solution: Using NPM Overrides
The correct, distinct, and persistent way to handle React 19 upgrades in an ecosystem that expects React 18 is using NPM Overrides.
Introduced in npm v8.3.0, overrides allows you to force specific dependency versions deep within the tree, overriding the preferences of your third-party libraries.
Step 1: Analyze the Breakage
Run the install command to see which packages are complaining.
npm install react@latest react-dom@latest
You will likely see output resembling this:
npm ERR! code ERESOLVE
npm ERR! ERESOLVE could not resolve
npm ERR!
npm ERR! While resolving: @mui/material@5.15.0
npm ERR! Found: react@19.0.0
npm ERR!
npm ERR! Conflicting peer dependency: react@18.2.0
npm ERR! node_modules/react
npm ERR! peer react@"^17.0.0 || ^18.0.0" from @mui/material@5.15.0
Step 2: Update package.json with Overrides
You need to instruct npm to treat the react and react-dom used by all dependencies as the version you have defined in your root package.json.
Open your package.json and add the overrides section. We use the $ syntax to reference the version installed in the project root. This ensures that when you update React later (e.g., to 19.1), the overrides automatically sync.
{
"name": "my-react-19-app",
"version": "1.0.0",
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"@mui/material": "^5.15.0"
},
"overrides": {
"react": "$react",
"react-dom": "$react-dom"
}
}
Step 3: Clean and Reinstall
For the override to take full effect and restructure the package-lock.json correctly, it is best practice to perform a clean install.
rm -rf node_modules package-lock.json
npm install
NPM will now force every library in your dependency tree to accept React 19, effectively silencing the peer dependency conflict errors.
Handling TypeScript Definition Conflicts
If you are using TypeScript, you might encounter a secondary issue. The @types/react libraries often have strict dependencies as well.
If you see type errors or conflicts with @types, extend your overrides to include the type definitions.
"overrides": {
"react": "$react",
"react-dom": "$react-dom",
"@types/react": "$@types/react",
"@types/react-dom": "$@types/react-dom"
}
Ensure you have the latest types installed in your dev dependencies:
npm install -D @types/react@latest @types/react-dom@latest
Verifying the Installation
Once the installation completes successfully, verify that the dependency resolution worked as expected. You can inspect the tree to ensure only one version of React is present.
Run the following command:
npm ls react
Correct Output: You should see a "deduped" or singular reference to version 19.
my-react-19-app@1.0.0
├─┬ @mui/material@5.15.0
│ └── react@19.0.0 deduped
└── react@19.0.0
If you see multiple versions (e.g., react@18.2.0 nested under a library), the override was not applied correctly, and you risk runtime errors due to "dual React" instances.
Code: Testing Runtime Compatibility
Forcing the installation fixes the build time error, but you must verify runtime compatibility. React 19 introduces breaking changes, such as the removal of defaultProps for functional components.
Create a simple test component using a new React 19 feature, such as useActionState (formerly useFormState), to verify the environment is loaded correctly.
import { useActionState } from "react";
// Mock server action for demonstration
async function updateName(prevState: string, formData: FormData) {
await new Promise((resolve) => setTimeout(resolve, 500));
return formData.get("name") as string;
}
export default function VersionCheck() {
const [state, formAction, isPending] = useActionState(updateName, "Anonymous");
return (
<div style={{ padding: '2rem', fontFamily: 'system-ui' }}>
<h2>React Version: {import.meta.env.REACT_APP_VERSION || React.version}</h2>
<p>Current Name: {state}</p>
<form action={formAction}>
<input type="text" name="name" required style={{ padding: '8px' }} />
<button type="submit" disabled={isPending} style={{ marginLeft: '10px', padding: '8px' }}>
{isPending ? "Updating..." : "Update"}
</button>
</form>
</div>
);
}
If this component renders and the form submits, your React 19 instance is running correctly alongside your coerced libraries.
Risks and Edge Cases
While overrides resolves the dependency tree, it does not rewrite the library code. You are essentially telling npm to "trust you."
1. Breaking Changes in Libraries
Some libraries rely on internal React internals that may have changed in version 19. If you encounter runtime errors like dispatcher is null or issues with context, check if the library relies on deprecated lifecycle methods or legacy context APIs.
2. Yarn and PNPM Users
If you are not using npm, the syntax differs slightly:
- Yarn: Use
resolutionsinpackage.json(works identically tooverrides). - PNPM: Use
pnpm.overridesor hooks in.pnpmfile.cjs.
3. The "Dual React" Hazard
Never allow two versions of React to run in the same bundle. This breaks Hooks (invalid hook call errors) and Context. Always use npm ls react after applying overrides to ensure the tree is flat.
Conclusion
The ERESOLVE error is a safety mechanism, but for senior engineers working on the bleeding edge, it is often a hurdle that needs to be managed manually.
By using overrides in package.json, you create a deterministic, reproducible build environment that allows you to leverage React 19 features immediately, without waiting for the entire open-source ecosystem to release patch updates. This approach is cleaner than legacy flags and ensures your CI/CD pipelines remain robust.