You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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"
  }
}

核心问题

  1. 为何打包产物中会出现多份React和ReactDOM的导入语句?
  2. 如何确保打包产物仅包含每个依赖的单一导入,以避免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()
    ],
  },
  // ...其他配置项保持不变
]);

验证修复

  1. 执行npm run build,查看打包产物,仅保留一份React/ReactDOM导入和一份react/jsx-runtime导入(若需JSX支持)。
  2. 将库安装到宿主应用测试,确认cannot import ... of undefined错误和Context异常消失。

内容的提问来源于stack exchange,提问作者Iyobosa Jefferson

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 19:25:54