Rollup打包React库出现重复React/ReactDOM导入问题及解决诉求
问题描述
使用Rollup打包自定义React npm库时,产物中出现多份React和ReactDOM的重复导入,示例如下:
import { jsx, Fragment, jsxs } from 'react/jsx-runtime'; import * as React from 'react'; import React__default, { useLayoutEffect, useEffect, forwardRef, useState, useRef, useCallback, useMemo, createContext, useContext, Fragment as Fragment$1, isValidElement, cloneElement, createElement, useId as useId$2, useReducer, useSyncExternalStore, createRef } from 'react'; import * as ReactDOM from 'react-dom'; import ReactDOM__default, { createPortal, flushSync } from 'react-dom';
这导致宿主应用出现cannot import (useContext | useState ...) of undefined错误,同时引发Context异常并增大包体积。
当前Rollup配置:
import commonjs from "@rollup/plugin-commonjs"; import resolve from "@rollup/plugin-node-resolve"; import typescript from "@rollup/plugin-typescript"; import autoprefixer from "autoprefixer"; import postcssImport from "postcss-import"; import { defineConfig } from "rollup"; import dts from "rollup-plugin-dts"; import external from "rollup-plugin-peer-deps-external"; import postcss from "rollup-plugin-postcss"; import tailwindcss from "tailwindcss"; import packageJson from "./package.json" assert { type: "json" }; export default defineConfig([ { input: "src/index.ts", output: [ { file: packageJson.main, format: "cjs", sourcemap: true, }, { file: packageJson.module, format: "esm", sourcemap: true, }, ], onwarn(warning, warn) { // https://github.com/TanStack/query/issues/5175 if (warning.code === "MODULE_LEVEL_DIRECTIVE") { return; } warn(warning); }, plugins: [ external(), resolve({ skip: ["react", "react-dom"], }), commonjs(), typescript({ tsconfig: "./tsconfig.json", declaration: true, declarationDir: "./dist", exclude: ["**/*.stories.tsx", "**/*.test.ts", "**/*.test.tsx"], }), ], }, { input: "src/styles/global.css", output: [{ file: "dist/index.css", format: "es" }], plugins: [ postcss({ extract: true, minimize: true, plugins: [postcssImport(), tailwindcss(), autoprefixer()], }), ], }, { input: "src/index.ts", output: [{ file: "dist/index.d.ts", format: "es" }], plugins: [dts.default()], }, ]);
package.json配置:
{ "name": "xx", "version": "0.0.5", "description": "", "repository": "xxx", "type": "module", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", "types": "dist/index.d.ts", "files": [ "dist" ], "scripts": { "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", "lint": "eslint . --ext .ts,.tsx --fix", "build": "rollup -c --bundleConfigAsCjs", "dev": "rollup -c -w --bundleConfigAsCjs", "release": "standard-version && git push --follow-tags && npm publish" }, "devDependencies": { "@chromatic-com/storybook": "^1.5.0", "@rollup/plugin-commonjs": "^26.0.1", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^11.1.6", "@storybook/addon-essentials": "^8.1.10", "@storybook/addon-interactions": "^8.1.10", "@storybook/addon-links": "^8.1.10", "@storybook/addon-onboarding": "^8.1.10", "@storybook/addon-styling-webpack": "^1.0.0", "@storybook/addon-themes": "^8.1.10", "@storybook/blocks": "^8.1.10", "@storybook/react": "^8.1.10", "@storybook/react-vite": "^8.1.10", "@storybook/test": "^8.1.10", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@typescript-eslint/eslint-plugin": "^6.9.0", "@vitejs/plugin-react": "^4.3.1", "autoprefixer": "^10.4.19", "clsx": "^2.1.1", "eslint": "8.54.0", "eslint-config-prettier": "^9.1.0", "eslint-import-resolver-typescript": "^3.6.1", "eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-promise": "^6.1.1", "eslint-plugin-react": "^7.33.2", "eslint-plugin-regexp": "^2.2.0", "eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-storybook": "^0.8.0", "postcss": "^8.4.38", "postcss-import": "^16.1.0", "prettier": "^3.1.0", "prettier-plugin-tailwindcss": "^0.5.11", "react-icons": "^5.2.1", "rollup": "^4.19.0", "rollup-plugin-dts": "^6.1.1", "rollup-plugin-peer-deps-external": "^2.2.4", "rollup-plugin-postcss": "^4.0.2", "standard-version": "^9.5.0", "storybook": "^8.1.10", "tailwind-merge": "^2.3.0", "tailwindcss": "^3.4.1", "tailwindcss-animate": "^1.0.7", "typescript": "^5.4.5", "vite-tsconfig-paths": "^4.3.2" }, "dependencies": { "@headlessui/react": "^2.1.2", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-checkbox": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-radio-group": "^1.2.0", "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-switch": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0", "@radix-ui/react-toast": "^1.2.1", "@radix-ui/react-tooltip": "^1.1.1", "@tanstack/react-table": "^8.19.3", "class-variance-authority": "^0.7.0", "classname-variants": "^1.5.0", "input-otp": "^1.2.4", "next-themes": "^0.3.0", "react-icons": "^5.2.1", "react-phone-number-input": "^3.4.5", "sonner": "^1.5.0", "tslib": "^2.6.3" }, "peerDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0" } }
核心问题
- 为何打包产物中会出现多份React和ReactDOM的导入语句?
- 如何确保打包产物仅包含每个依赖的单一导入,以避免Context问题并减小包体积?
已尝试方案
- 在Rollup配置中将React和ReactDOM设为外部依赖
- 确保源码中导入React的方式一致
但问题仍存在,需要解决建议。
期望结果
- 打包产物中仅包含单一的React和ReactDOM导入
- 解决宿主应用中
cannot import (useContext | useState ...) of undefined错误
解决方案
1. 重复导入的原因
- 第三方依赖导入不一致:你使用的
@headlessui/react、@radix-ui/*等依赖内部可能混用了不同的React导入方式,比如默认导出、命名导出、react/jsx-runtime导入,Rollup的external插件仅标记外部依赖,不会主动合并这些不同形式的导入。 - CommonJS转ES模块残留:
@rollup/plugin-commonjs转换CommonJS格式依赖时,若依赖内部混用ES和CommonJS模块,可能生成额外的React导入。 - JSX编译独立导入:TypeScript编译JSX时自动导入的
react/jsx-runtime不会被peer-deps-external处理,导致和其他React导入并存。
2. 具体修复步骤
步骤一:明确标记所有React相关外部依赖
替换external()插件调用,手动指定所有React相关模块为外部依赖:
// 在第一个配置对象中添加 external: [ 'react', 'react-dom', 'react/jsx-runtime', 'react/jsx-dev-runtime' ], // 移除原来的 external() 插件
注:
react/jsx-runtime和react/jsx-dev-runtime是JSX编译的核心依赖,必须标记为外部,否则会重复打包或生成冗余导入。
步骤二:优化CommonJS插件配置
给@rollup/plugin-commonjs添加排除规则,避免处理React相关模块:
commonjs({ exclude: ['node_modules/react/**', 'node_modules/react-dom/**'] }),
步骤三:配置TypeScript使用外部JSX运行时
修改tsconfig.json,确保JSX编译依赖外部的react/jsx-runtime:
{ "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "react", "module": "ESNext", "moduleResolution": "NodeNext", "skipLibCheck": true } }
步骤四:添加代码压缩合并导入
使用@rollup/plugin-terser压缩代码,自动合并重复导入:
// 导入插件 import terser from '@rollup/plugin-terser'; // 在第一个配置的plugins数组中添加 plugins: [ // ...其他插件 terser() ],
步骤五:确认package.json依赖配置
确保react和react-dom仅出现在peerDependencies中,不要在dependencies或devDependencies中重复声明(当前配置已符合,无需修改)。
最终修改后的Rollup核心配置片段
export default defineConfig([ { input: "src/index.ts", output: [ { file: packageJson.main, format: "cjs", sourcemap: true, }, { file: packageJson.module, format: "esm", sourcemap: true, }, ], external: [ 'react', 'react-dom', 'react/jsx-runtime', 'react/jsx-dev-runtime' ], onwarn(warning, warn) { if (warning.code === "MODULE_LEVEL_DIRECTIVE") { return; } warn(warning); }, plugins: [ resolve({ skip: ["react", "react-dom"], }), commonjs({ exclude: ['node_modules/react/**', 'node_modules/react-dom/**'] }), typescript({ tsconfig: "./tsconfig.json", declaration: true, declarationDir: "./dist", exclude: ["**/*.stories.tsx", "**/*.test.ts", "**/*.test.tsx"], }), terser() ], }, // ...其他配置项保持不变 ]);
验证修复
- 执行
npm run build,查看打包产物,仅保留一份React/ReactDOM导入和一份react/jsx-runtime导入(若需JSX支持)。 - 将库安装到宿主应用测试,确认
cannot import ... of undefined错误和Context异常消失。
内容的提问来源于stack exchange,提问作者Iyobosa Jefferson
相关产品推荐
相关产品推荐

