How to Fix: Can’t upgrade project to Next.js 15 when using workspace React package
Upgrading to Next.js 15 can fail in a monorepo the moment your app consumes a workspace React package. The breakage usually looks confusing at first: the app appears to have React installed correctly, but Next detects an incompatible or duplicated React setup and refuses to proceed. In practice, this happens because Next.js 15 has stricter expectations around React resolution, package boundaries, and how workspace dependencies expose react and react-dom.
Understanding the Root Cause
This issue typically appears in a workspace setup where one package, such as a shared UI or React component library, is consumed by a Next.js app. The shared package may list react in the wrong dependency field, may pull in its own React copy, or may be exported in a way that causes the app and the package to resolve different module instances.
With Next.js 15, your application is expected to run against a single compatible React tree, usually React 19 RC or the exact React version required by the Next release you are upgrading to. If your workspace package declares react and react-dom as regular dependencies instead of peerDependencies, Bun can install another copy inside that workspace package. Once that happens, Next sees multiple React instances or a version mismatch.
The most common technical causes are:
- Your shared workspace package lists react or react-dom under dependencies instead of peerDependencies.
- The app and the shared package resolve different React versions.
- The package is not built or exported correctly for consumption by Next.js.
- Bun workspaces hoist packages differently than expected, exposing duplicate resolution paths.
- The Next app imports source files from the workspace package without the right transpilePackages setup.
In short, the bug is not usually that Next.js 15 cannot work with workspaces. The real issue is that the workspace package behaves like it owns React, when in a monorepo it should usually treat React as a peer provided by the app.
Step-by-Step Solution
The fix is to make your workspace React package behave like a proper shared library and ensure the Next app is the one providing React.
1. Move React and React DOM to peerDependencies in the workspace package
Open the package.json of the shared workspace package and make sure it looks like this pattern:
{
"name": "@your-scope/ui",
"version": "1.0.0",
"main": "./src/index.ts",
"types": "./src/index.ts",
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"typescript": "^5.0.0"
}
}
Important: remove React from dependencies if it exists there. Keeping it in both places can still lead to duplicate installs.
2. Align React versions across the monorepo
In the Next.js app package, use the exact React version expected by your Next.js 15 release. For example:
{
"name": "web",
"dependencies": {
"next": "15.0.0",
"react": "19.0.0-rc.0",
"react-dom": "19.0.0-rc.0",
"@your-scope/ui": "workspace:*"
}
}
If the reproduction repo uses a specific React RC version, match that version exactly in every relevant package.
3. Configure Next.js to transpile the workspace package
If your shared package ships raw TypeScript or modern ESM source, tell Next to transpile it:
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ['@your-scope/ui']
}
module.exports = nextConfig
This is especially important when importing directly from workspace source files.
4. Remove old installs and lockfiles, then reinstall
After changing dependency fields, clear the install state so Bun rebuilds the graph correctly:
rm -rf node_modules
rm -f bun.lockb
bun install
If the repo has nested node_modules directories inside workspace packages, remove those too.
5. Verify that only one React version is resolved
Use Bun to inspect the installed dependency graph:
bun pm ls react
bun pm ls react-dom
You want to confirm that the app and the shared workspace package both resolve to the same top-level React installation.
6. If needed, build the shared package explicitly
Some workspace packages work better when they expose a built output instead of raw source. A safer package configuration can look like this:
{
"name": "@your-scope/ui",
"version": "1.0.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}
Then build it before running the Next app:
bun run build
7. Start the app again
bun run dev
If the issue was caused by duplicate or mis-scoped React dependencies, the app should now upgrade cleanly to Next.js 15.
Recommended working pattern for monorepos
- The Next.js app owns react and react-dom.
- Shared React packages declare them as peerDependencies.
- Shared packages may keep React in devDependencies only for local development and typechecking.
- Use transpilePackages when consuming unbuilt workspace source.
Common Edge Cases
1. React is still duplicated after moving to peerDependencies
This usually means an old lockfile or nested install is still present. Delete every node_modules folder, remove the lockfile, and reinstall.
2. The package works in npm or pnpm but fails in Bun
Bun workspaces can expose dependency layout issues more visibly. If your package metadata is slightly wrong, Bun may still install in a way that reveals duplicate React resolution. The fix is still the same: correct peerDependencies and version alignment.
3. Next.js throws errors about ESM, TypeScript, or unexpected tokens
That usually means the workspace package is being consumed as source but is not transpiled. Add it to transpilePackages or prebuild the package to dist.
4. Server Components and Client Components start failing
If your shared package includes hooks or browser-only code, ensure files that need client execution include the ‘use client’ directive where appropriate. Next.js 15 is stricter around React Server Component boundaries.
5. The shared package pins a different React prerelease
Even a small mismatch between React RC versions can break the app. Use the same React and react-dom versions across the entire workspace.
FAQ
Why does this only happen after upgrading to Next.js 15?
Next.js 15 tightens integration with newer React versions and makes package resolution problems more obvious. A workspace setup that was loosely tolerated before can fail once React version expectations become stricter.
Should a shared UI package ever include React in dependencies?
Usually no. A reusable React package in a monorepo should expose react and react-dom as peerDependencies, so the consuming app provides the actual runtime instance.
Do I always need transpilePackages for workspace packages?
No. If the package is already built to compatible JavaScript and exported correctly, you may not need it. But if the package exposes raw TypeScript, JSX, or modern source directly, transpilePackages is often required.
The reliable fix for this GitHub issue is to treat the workspace package as a proper shared library: single React owner, aligned versions, peerDependencies, and correct transpilation. Once those pieces are in place, upgrading a monorepo app to Next.js 15 becomes predictable instead of fragile.