Fixing 'Module not found: Can't resolve react/jsx-runtime' in Webpack 5 Component Libraries with Next.js 12
Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 10–20 mins
TL;DR
- What broke: Your component library's Webpack 5 bundle is shipping its own copy of
react/jsx-runtimeinstead of externalizing it, causing a module resolution collision or a missing-module hard crash in the consuming Next.js 12 app. - How to fix it: Explicitly add
react/jsx-runtime(alongsidereactandreact-dom) to theexternalsarray in your library'swebpack.config.js, and declare it inpeerDependenciesinpackage.json. - Fast path: Use our Client-Side Sandbox below to auto-refactor this — paste your
webpack.config.jsandpackage.jsonand get corrected output without sending your config to a third-party server.
The Incident (What does the error mean?)
Raw error output from next build or webpack CLI:
Module not found: Can't resolve 'react/jsx-runtime'
> in ./node_modules/your-component-lib/dist/index.js
error - ./node_modules/your-component-lib/dist/index.js
Module not found: Can't resolve 'react/jsx-runtime'
Immediate consequence: The Next.js 12 build halts entirely. No static pages are generated. Production deployment is blocked. This is not a warning — it is a hard fatal error.
The underlying mechanism: React 17+ introduced the new JSX transform, which auto-imports from react/jsx-runtime instead of requiring React in scope. When your library is compiled with the new JSX transform enabled (default in most modern Babel/TSC configs) but react/jsx-runtime is not externalized, Webpack 5 bundles a reference to that module path into your dist/ output. The consuming app then tries to resolve react/jsx-runtime from inside node_modules/your-component-lib/dist/, where it does not exist.
The Attack Vector / Blast Radius
This is a dependency isolation failure, not a security CVE — but the blast radius in a monorepo or shared design system is severe:
- Every downstream app breaks simultaneously. If your component library is published to a private registry and consumed by 5 Next.js apps, a single bad publish takes down all 5 builds.
- Duplicate React instances. Even if the consuming app has React installed, if
react/jsx-runtimeis bundled inside your library dist instead of externalized, you risk two separate React runtime instances in the same browser context. This causes theInvalid hook callerror and breaks all hooks silently at runtime — harder to debug than the build error. - Next.js 12 + Webpack 5 module resolution is stricter. Webpack 5 removed automatic polyfilling of Node.js core modules and tightened
exportsfield resolution.react/jsx-runtimerelies on theexportsfield in React'spackage.json. If your library bundles it, the path resolution inside the dist bundle does not have the correctnode_modulescontext to walk up to. - CI/CD pipelines fail silently on cache hits. If your pipeline caches
node_modulesand the bad library version is cached, the error reappears after cache invalidation, making it look like an infrastructure flake rather than a code defect.
How to Fix It (The Solution)
Basic Fix — Externalize react/jsx-runtime in your library's Webpack config
// webpack.config.js (library bundle)
externals: {
- react: 'react',
- 'react-dom': 'react-dom',
+ react: 'react',
+ 'react-dom': 'react-dom',
+ 'react/jsx-runtime': 'react/jsx-runtime',
+ 'react/jsx-dev-runtime': 'react/jsx-dev-runtime',
},
Why both
jsx-runtimeandjsx-dev-runtime? Babel and TSC emitjsx-dev-runtimeimports in development mode builds. Externalizing onlyjsx-runtimeleaves dev builds broken.
Fix package.json — Declare correct peerDependencies
// package.json (your component library)
"peerDependencies": {
- "react": ">=16.8.0",
- "react-dom": ">=16.8.0"
+ "react": ">=17.0.0",
+ "react-dom": ">=17.0.0"
},
+ "peerDependenciesMeta": {
+ "react": { "optional": false },
+ "react-dom": { "optional": false }
+ }
Bumping the peer dep floor to
>=17.0.0is intentional —react/jsx-runtimedoes not exist in React 16. If you must support React 16, you need a Babel config that uses the classic JSX runtime (@babel/plugin-transform-react-jsxwithruntime: 'classic').
Enterprise Best Practice — Use externalsType + regex for future-proofing
// webpack.config.js
module.exports = {
mode: 'production',
output: {
library: { type: 'commonjs2' },
},
- externals: {
- react: 'react',
- 'react-dom': 'react-dom',
- },
+ externalsType: 'commonjs',
+ externals: [
+ // Externalize all react/* subpath exports
+ /^react(\/.*)?$/,
+ /^react-dom(\/.*)?$/,
+ ],
};
Using a regex pattern (/^react(\/.*)$/) future-proofs against any new React subpath exports (e.g., react/compiler-runtime in React 19+) without requiring manual config updates per release.
Bonus — Verify with webpack-bundle-analyzer
After rebuilding, confirm react and react/jsx-runtime are not present in your dist bundle:
npx webpack-bundle-analyzer dist/stats.json
# react/* should appear as external, not as a bundled module chunk
💡 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. Add a bundle validation step post-build
# In your library's CI pipeline (GitHub Actions / GitLab CI)
- name: Assert react is externalized
run: |
if grep -r '"react/jsx-runtime"' dist/; then
echo "FATAL: react/jsx-runtime is bundled. Externalization failed."
exit 1
fi
2. Use publint to catch peer dependency issues before publish
npx publint
# Catches: missing peerDependencies, incorrect exports fields, CJS/ESM mismatches
Add to your prepublishOnly script:
// package.json
"scripts": {
+ "prepublishOnly": "publint && npm run build"
}
3. Enforce with are-the-types-wrong + attw
npx @arethetypeswrong/cli ./dist/your-lib.tgz
# Validates that module resolution behaves correctly for CJS and ESM consumers
4. Lock peer dep validation in PR checks
If using Nx or Turborepo, add a lint rule to your project.json or turbo.json that runs publint on every affected library package before the build task executes. This catches the misconfiguration at PR time, not at publish time.