Rollup bundling peer dependencies / Multiple instances of MUI make the ThemeProvider not work

Viewed 1076

I have two different repos I'm working on, lib and apps.

  • lib has some React components made with MUI that I want to consume in apps, and that's built with Rollup.

  • apps is a Lerna monorepo with a Next.js app that imports lib as a dependency to consume its components, which can be customized wrapping them in a <ThemeProvider> with a custom theme.

However, that doesn't work because there are duplicated instances of MUI / ThemeProvider, which I was able to verify by adding this to lib's entry point:

if (process.browser) {
  (window as any)._React = React;
  (window as any)._ThemeProvider = ThemeProvider;
}

And then this into apps':

if (process.browser) {
  const {
    _React,
    _ThemeProvider,
  } = window as any;

  console.log(_React ? (_React === React ? "✅ Unique React" : "❌ Duplicate React") : "-");
  console.log(_ThemeProvider ? (_ThemeProvider === ThemeProvider ? "✅ Unique ThemeProvider" : "❌ Duplicate ThemeProvider") : "-");
}

Which prints:

✅ Unique React
❌ Duplicate ThemeProvider

lib's component usage in app looks like this:

<ThemeProvider theme={ MY_CUSTOM_THEME }>
  <MyComponent{ ...myComponentProps } />
</ThemeProvider>

Where both MyComponent and MY_CUSTOM_THEME are imported from lib, while ThemeProvider is imported from @mui/material/styles, just like it is in lib.

However, all MUI components will be displayed with the default theme.

Here are some of the relevant build files for both repos:

lib > package.json:

{
  "name": "@myscope/lib",
  "version": "1.0.0-alpha",
  "description": "",
  "main": "dist/cjs/src/index.js",
  "module": "dist/esm/src/index.js",
  "types": "dist/index.d.ts",
  "files": [
    "dist"
  ],
  "private": true,
  "scripts": {
    "build": "rollup -c",
  },
  "dependencies": {
    "@apollo/client": "^3.4.5",
    "@apollo/link-context": "^2.0.0-beta.3",
    "@hookform/resolvers": "^2.8.5",
    "@mui/icons-material": "^5.0.4",
    "@swyg/corre": "^1.0.1",
    "apollo-upload-client": "^16.0.0",
    "atob": "^2.1.2",
    "axios": "^0.24.0",
    "btoa": "^1.2.1",
    "country-codes-list": "^1.6.8",
    "country-region-data": "^1.6.0",
    "next": "^11.0.1",
    "next-images": "^1.8.2",
    "openpgp": "^5.0.0",
    "react-hook-form": "^7.22.0",
    "react-payment-inputs": "^1.1.8",
    "react-use-country-region": "^1.0.0",
    "styled-components": "^5.3.0",
    "use-callback-ref": "^1.2.5",
    "uuidv4": "^6.2.12",
    "yup": "^0.32.11"
  },
  "peerDependencies": {
    "@mui/material": "^5.0.4",
    "react-dom": "^17.0.2",
    "react": "^17.0.2"
  },
  "devDependencies": {
    "@auth0/auth0-react": "^1.6.0",
    "@babel/preset-react": "^7.16.7",
    "@emotion/react": "^11.5.0",
    "@emotion/styled": "^11.3.0",
    "@graphql-codegen/cli": "^1.21.5",
    "@graphql-codegen/introspection": "^1.18.2",
    "@graphql-codegen/typescript-operations": "^1.18.0",
    "@graphql-codegen/typescript-react-apollo": "^2.2.5",
    "@graphql-codegen/typescript": "^1.22.1",
    "@mui/material": "^5.2.8",
    "@rollup/plugin-babel": "^5.3.0",
    "@rollup/plugin-commonjs": "^21.0.1",
    "@rollup/plugin-node-resolve": "^13.1.2",
    "@rollup/plugin-typescript": "^8.3.0",
    "@testing-library/dom": "^8.1.0",
    "@testing-library/jest-dom": "^5.14.1",
    "@testing-library/react": "^12.0.0",
    "@testing-library/user-event": "^13.2.1",
    "@types/jest": "^26.0.24",
    "@types/react-dom": "^17.0.11",
    "@types/react": "^17.0.16",
    "@types/styled-components": "^5.1.12",
    "@typescript-eslint/eslint-plugin": "^4.29.0",
    "@typescript-eslint/parser": "^4.29.0",
    "babel-jest": "^27.0.6",
    "babel-plugin-styled-components": "^2.0.2",
    "eslint-config-next": "^11.0.1",
    "eslint-config-prettier": "^8.3.0",
    "eslint-plugin-react-hooks": "^4.2.0",
    "eslint-plugin-unused-imports": "^1.1.2",
    "eslint": "^7.32.0",
    "graphql": "^16.2.0",
    "jest": "^27.0.6",
    "prettier": "^2.3.2",
    "rollup-plugin-dts": "^4.1.0",
    "rollup-plugin-node-externals": "^3.1.2",
    "rollup-plugin-peer-deps-external": "^2.2.4",
    "rollup-plugin-typescript2": "^0.31.1",
    "rollup": "^2.63.0",
    "ts-jest": "^27.0.4",
    "typescript": "^4.3.5"
  }
}

Note that @mui/material appears both in peerDependencies and devDependencies, but not react or rect-dom, which are only listed as peerDependencies. That's because there's @types/react and @types/react-dom, but nothing similar for MUI, so the Rollup build would fail if it can't find the right types.

lib > rollup.config.js:

import commonjs from '@rollup/plugin-commonjs';
import dts from 'rollup-plugin-dts'
import resolve from "@rollup/plugin-node-resolve";
import typescript from 'rollup-plugin-typescript2'
import babel from '@rollup/plugin-babel'

import pkg from "./package.json";

// Extensions handled by babel:
const EXTENSIONS = [".ts", ".tsx"];

// Exclude dev and peer dependencies:
const EXTERNAL = [
  ...Object.keys(pkg.devDependencies),
  ...Object.keys(pkg.peerDependencies),
];

export default [{
    input: 'src/index.ts',

    output: [{
        dir: "dist/esm",
        sourcemap: true,
        format: "esm",
        preserveModules: true,
        globals: {
          react: "React",
        },
    }, {
        dir: "dist/cjs",
        sourcemap: true,
        format: "cjs",
        preserveModules: true,
        globals: {
          react: "React",
        },
    }],

    external: EXTERNAL,
    
    plugins: [
       resolve(),

        commonjs({
            exclude: "node_modules",
            ignoreGlobal: true,
        }),

        typescript({
          useTsconfigDeclarationDir: true,
          tsconfig: "rollup.tsconfig.json",
        }),
    ],

}, {
    input: './dist/types/src/index.d.ts',
    output: [{ file: './dist/index.d.ts', format: "esm" }],
    external: EXTERNAL,
    plugins: [dts()],
}];

apps > lerna.json:

{
  "packages": ["apps/**/*", "packages/**/*"],
  "version": "0.0.1",
  "npmClient": "yarn",
  "useWorkspaces": true
}

apps > package.json:

...
"dependencies": {
    "@myscope/lib": "file:../lib"
},
...

Note this is a file: import just to make it easier to develop and test changes.

I've also tried adding resolutions to both apps > package.json and apps > app > package.json, but no luck with that either...:

"resolutions": {
  "@mui/material": "5.2.8"
}
1 Answers

It might not be the best solution, but I fixed this by exporting the ThemeProvider from lib:

export { ThemeProvider as MyComponentThemeProvider } from "@mui/material/styles";

Now, when using this one in app:

<MyComponentThemeProvider theme={ MY_CUSTOM_THEME }>
  <MyComponent{ ...myComponentProps } />
</MyComponentThemeProvider>

Everything works as expected:

✅ Unique React
✅ Unique ThemeProvider

Also, take a look at https://github.com/facebook/react/issues/19541

Related