reactjswebpackemotionmicro-frontendcss-in-js

How to exernalize @emotion/react, and @emotion/styled for Create React App


I have the following architecture:

main-frontend, foo-frontend, bar-frontend and baz-frontend are all standard React apps generated using create-react-app.

All of them except main-frontend publish their UI as a React component, wrapped in a Web component. These Web components can be loaded into the main-frontend to weave all 3 sub-frontends into one super-frontend.

The problem I am facing is that in order for this approach to work, all 4 frontends have to externalize react, react-dom, react-router, and any CSS-in-JS frameworks being used.

I am able to modify Webpack configuration for create-react-app generated React apps, using the rewire package and a couple of scripts. I am reproducing these below for others facing a similar problem.

In each of foo-frontend, bar-frontend, and baz-frontend,

package.json

  "dependencies": {
    ...
    "rewire": "^6.0.0"
    ...
  },
  "scripts": {
    "start": "node start.js",
    "build": "node build.js",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "webpackConfig": {
    "externals": {
      "react": "react",
      "react-dom": "react-dom",
      "@emotion/react": "@emotion/react",
      "@emotion/styled": "@emotion/styled"
    }
  }

build.js

const rewire = require('rewire');
const defaults = rewire('react-scripts/scripts/build.js');

const {
  webpackConfig: {
    externals
  }
} = require('./package.json');

const config = defaults.__get__('config');

config.externals = externals;

start.js

const rewire = require('rewire');
const defaults = rewire('react-scripts/scripts/start.js');
const webpackConfig = require('react-scripts/config/webpack.config');

const {
  webpackConfig: {
    externals
  }
} = require('./package.json');

defaults.__set__('configFactory', (webpackEnv) => {
  const config = webpackConfig(webpackEnv);

  config.externals = externals;

  return config;
});

public/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    ...
    <title>Atlas Reviews</title>
    <script src="https://cdn.jsdelivr.net/npm/react@18.1.0/umd/react.production.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/react-dom@18.1.0/umd/react-dom.production.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@emotion/react@11.9.0/dist/emotion-react.umd.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@emotion/styled@11.8.1/dist/emotion-styled.umd.min.js"></script>
  </head>
  <body>
  ...
  </body>
</html>

The problem I am getting is that @emotion/react and @emotion/styled aren't being externalized correctly.

Uncaught ReferenceError: emotion is not defined
    at Object.@emotion/react (external var "emotion":1:1)
    at Object.options.factory (react refresh:6:1)
    at __webpack_require__ (bootstrap:24:1)
    at fn (hot module replacement:62:1)
    at Module../src/components/App/styles.tsx (index.tsx:25:1)
    at Module.options.factory (react refresh:6:1)
    at __webpack_require__ (bootstrap:24:1)
    at fn (hot module replacement:62:1)
    at Module../src/components/App/index.tsx (logo.svg:34:1)
    at Module.options.factory (react refresh:6:1)

How do I fix this?


Solution

  • I managed to solve this, not with @emotion/react and @emotion/styled, but with styled-components.

    To be fair, that was my initial preference for a CSS-in-JS framework.

    In my previous attempts, I had been using the wrong version of the styled-components library.

    Here's how I managed to do it:

    package.json in all 4 React apps should be modified like so.

    {
      ...
      "dependencies": {
        ...
        "@types/styled-components": "^5.1.25",
        "rewire": "^6.0.0",
        "styled-components": "^5.3.5",
        ...
      },
      "scripts": {
        "start": "PORT=3004 node start.js",
        "build": "PORT=3004 node build.js",
        "test": "PORT=3004 react-scripts test",
        "eject": "PORT=3004 react-scripts eject"
      },
      "webpackConfig": {
        "externals": {
          "react": "React",
          "react-dom": "ReactDOM",
          "react-is": "ReactIS",
          "styled-components": "styled"
        }
      },
      ...
    }
    

    public/index.html in the 3 sub-frontends should be modified like so:

    <!DOCTYPE html>
    <html lang="en">
      <head>
        ...
        <title>Atlas Reviews</title>
        <script src="https://cdn.jsdelivr.net/npm/react@18.1.0/umd/react.production.min.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/react-dom@18.1.0/umd/react-dom.production.min.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/react-is@18.1.0/umd/react-is.production.min.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/styled-components@5.3.5/dist/styled-components.min.js"></script>
        ...
      </head>
      <body>
      ...
      </body>
    </html>
    

    The public/index.html for the super-frontend will be similar, but will also include script tags to import the sub-frontends:

    <!DOCTYPE html>
    <html lang="en">
      <head>
        ...
        <title>Atlas Reviews</title>
        <script src="https://cdn.jsdelivr.net/npm/react@18.1.0/umd/react.production.min.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/react-dom@18.1.0/umd/react-dom.production.min.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/react-is@18.1.0/umd/react-is.production.min.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/styled-components@5.3.5/dist/styled-components.min.js"></script>
        <script src="http://localhost:3001/static/js/bundle.js"></script>
        <script src="http://localhost:3002/static/js/bundle.js"></script>
        <script src="http://localhost:3003/static/js/bundle.js"></script>
        ...
      </head>
      <body>
      ...
      </body>
    </html>
    

    TL;DR;: Note the externals for react-is and styled-components and the version of the styled-components script being loaded via CDN.