Micro frontend (1) – How to create a reusable component library (1)?

Method 1

First thing first

Number 1

  • You can develop an independent component in a separate folder and expose it out using ModuleFederationPlugin
  • State configuration (store configuration), theme configuration can be written in a separate providers component
// Providers.js

// import anything that you need
import React from 'react';
import { Provider as ReduxProvider } from 'react-redux';
import { ThemeProvider } from 'emotion-theming';
import { store } from './configureStore';
import theme from './theme';
import './index.css';

const MyProviders = ({ children }) => (
  <ReduxProvider store={store}>
    <ThemeProvider theme={theme}>{children}</ThemeProvider>
  </ReduxProvider>
);


export default MyProviders;
  • Wrap your exposed component with the above provider
// MyExposedComponentWrapper.js

import React from 'react';
import { hot } from 'react-hot-loader/root';
import MyExposedComponent from 'components/MyExposedComponent';
import MyProviders from 'providers';

// Wrap your exposed component
export default hot((props) => (
  <MyProviders>
    <MyExposedComponent {...props} />
  </MyProviders>
));
  • Now, you can freely expose your component using ModuleFederationPlugin

Number 2

Life is easier if you can expose your remote React library components as React components. By doing this, your React host app just get and use them as React components.

However, in micro front end architecture, it is recommended not to expose your components as React components but as inner html. Why? by doing this, it’s possible to use your library components no matter what framework your host app is using.

How to render inner HTML codes in React?

// ./components/MarketingApp.js

import { mount } from 'RemoteLib/Header'; // mount(htmlElement) function is to add inner html codes into a HTML element
import React, { useRef, useEffect } from 'react';

export default () => {
  const ref = useRef(null);

  useEffect(() => {
    mount(ref.current)
  });

  return <div ref={ref} />
};

How to expose your component?

Library side

  • Expose modules using ModuleFederationPlugin
    webpack configuration
const { ModuleFederationPlugin } = require('webpack').container

...

output: {
  filename: '[name].bundle.js',
  path: path.resolve(__dirname, './dist'),
  publicPath: '',
},

...

plugins: [
  new ModuleFederationPlugin({
    name: 'My-Library',
    library: { type: 'var', name: 'lib' },
    fileName: 'remoteEntry.js',
    exposes: {
      './Button': './src/components/hello-world/button.js'
    }
  })
]
  • Build and copy ‘remoteEntry.js’ into cdn
  • Set public url of cdn
    webpack configuration
  output: {
    ...
    publicPath: 'http://localhost:9001/',
  },

App side

  • Configure Webpack
const { ModuleFederationPlugin } = require('webpack').container

...

plugins: [
  new ModuleFederationPlugin({
    name: 'MyApp',
    remotes: {
      'UILibrary': 'lib@http://localhost:9001/remoteEntry.js'
    }
  })
]
  • Refactor entry point to load asynchronously
// bootstrap.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';


ReactDOM.render(
  <App />,
  document.getElementById(
    'root'
  )
);
// index.js

import './bootstrap'
// index.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <title>React Boilerplate with Webpack and Tailwind 2</title>

  <!-- Library -->
  <script src="<%= libRemoteEntry %>"></script>
</head>

<body>
  <div id="root"></div>
</body>

</html>
// webpack config

const { dependencies } = require('../package.json');
const deps = dependencies;

...

    new ModuleFederationPlugin({
      library: { type: 'var' },
      name: 'MyApp',
      remotes: {
        'UILibrary': 'lib'
      },
      shared: {
        ...deps,
        react: {
          eager: true,
          singleton: true,
          requiredVersion: deps.react,
        },
        "react-dom": {
          eager: true,
          singleton: true,
          requiredVersion: deps["react-dom"],
        },
      }
    }),

    new HtmlWebpackPlugin({
      template: paths.public + '/index.html', // template file
      filename: 'index.html', // output file
      templateParameters: {
        libRemoteEntry: 'http://localhost:9001/remoteEntry.js',
      },
    }),

Issues

Uncaught Error: Shared module is not available for eager consumption: webpack/sharing/consume/default/react/react

  • App side
    Add library type to remote, add “http://localhost:9001/remoteEntry.js'” into HTML template file
// webpack config

...

    new ModuleFederationPlugin({
      library: { type: 'var' },
      name: 'MyApp',
      remotes: {
        'UILibrary': 'lib'
      },
      ...
    }),

    new HtmlWebpackPlugin({
      template: paths.public + '/index.html', // template file
      filename: 'index.html', // output file
      templateParameters: {
        libRemoteEntry: 'http://localhost:9001/remoteEntry.js',
      },
    }),
// index.html

...

  <!-- Library -->
  <script src="<%= libRemoteEntry %>"></script>

...

React-Hot-Loader: AppContainer should be patched

or A cross-origin error was thrown. React doesn’t have access to the actual error object in development.

  • App side
    Add shared libraries
    new ModuleFederationPlugin({
      ...
      shared: {
        ...deps,
        react: {
          singleton: true,
          requiredVersion: deps.react,
        },
        "react-dom": {
          singleton: true,
          requiredVersion: deps["react-dom"],
        },
      }
    }),

Shared module is not available for eager consumption: webpack/sharing/consume/default/react/react

This is because when you define react in shared, you have made it async import
You need to create bootstrap.js file, move your codes from entry index.js file to bootstrap.js

// bootstrap.js

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import './index.scss'

ReactDOM.render(<App />, document.getElementById('root'))
// index.js

import('./bootstrap'). // async import

By using bootstrap.js, you don’t need to async import modules from remoteEntry anymore

// App.js

// No need to do this
// const Header = React.lazy(
//  () => import('UILibrary/header')
// );

import Header from 'UILibrary/header'

...

Method 2

This has been used before ModuleFederationPlugin is introduced. The downside of this is that in your target project you may have to install some dependencies which are used in your library project.

If you use Webpack ModuleFederationPlugin, needed dependencies bundles are all provided alongside with your exposed component.

Library side

  • Create a UI library project which has name as in package.json as below
    “name”: “@hung/common-ui”
  • Run “yarn link” from UI library project to register UI library project

App side

  • Inside app project, run “yarn add @hung/common-ui” to add UI library project into node_modules folder
  • From now on, you can import component from @hung/common-ui as below
    import Button from ‘@hung/common-ui/components/Button’;

Issues

How to use inherit tailwindcss from this library so that other project can use it?

  • Expose necessary css files using rollup
  // minified your own styles
  {
    input: 'src/style/style.css',
    output: {
      file: 'dist/style.css',
    },
    plugins: [
      postcss({
        extract: true,
        minimize: true,
      }),
    ],
  },


  // minified base styles
  {
    input: 'src/templates/tailwindcss/base.css',
    output: {
      file: 'dist/base.min.css',
    },
    plugins: [
      postcss({
        extract: true,
        minimize: true,
      }),
    ],
  },

  // minified components styles
  {
    input: 'src/templates/tailwindcss/components.css',
    output: {
      file: 'dist/components.min.css',
    },
    plugins: [
      postcss({
        extract: true,
        minimize: true,
      }),
    ],
  },

  // minified utilities styles
  {
    input: 'src/templates/tailwindcss/utilities.css',
    output: {
      file: 'dist/utilities.min.css',
    },
    plugins: [
      postcss({
        extract: true,
        minimize: true,
      }),
    ],
  },
  • Configure them in host project
// styles/index.js

import 'hung-ui/dist/base.min.css'
import 'hung-ui/dist/index.min.css'
import 'hung-ui/dist/components.min.css'
import 'hung-ui/dist/utilities.min.css'
// src/index.js

import './styles/index'
  • or you can configure it in template “index.html
// styles/utilities.css

@import 'hung-ui/dist/base.min.css'
// styles/utilities.css

@import 'hung-ui/dist/components.min.css'
// styles/utilities.css

@import 'hung-ui/dist/utilities.min.css'
// styles/utilities.css

@import 'hung-ui/dist/index.min.css'
// template index.html

<link type="text/css" href="/styles/base.css" />
<link type="text/css" href="/styles/index.css" />
<link type="text/css" href="/styles/components.css" />
<link type="text/css" href="/styles/utilities.css" />

How to inject style manually to override css style after compiling?

  • Set id for the css import inside template html file
<link id="my-tailwind-css" type="text/css" href="/styles/utilities.css" />
  • Create script file for style inject
// style-inject.js

const anchorNodeSelector = '#my-tailwind-css';

/**
 * We try to insert style above target node if it exists.
 * Otherwise just append to <head> tag.
 *
 * @param css the variable name that contains css content
 */
export default function styleInject(css) {
  if (!css || typeof document === 'undefined') {
    return;
  }

  const head = document.head || document.getElementsByTagName('head')[0];
  const anchorNode = document.querySelector(anchorNodeSelector);
  const container = anchorNode?.parentNode || head;

  const style = document.createElement('style');
  style.type = 'text/css';

  if (anchorNode) {
    container.insertBefore(style, anchorNode);
  } else {
    container.appendChild(style);
  }

  if (style.styleSheet) {
    style.styleSheet.cssText = css;
  } else {
    style.appendChild(document.createTextNode(css));
  }
}
  • Call this js file
// rollup.lib.config.js

...

// the react components
const config = components.map((component) => {
  return {
    ...
    
    plugins: [
    ...
      
      postcss({
        modules: true,
        inject: (cssVariableName, fileId) => {
          return (
            '\n' +
            `import styleInject from '../../utils/style-inject.js';` +
            '\n' +
            `styleInject(${cssVariableName})`
          );
        },
      }),
      json(),
    ],
  };
});

export default config;
// rollup.all.config.js

import base from './rollup.base.config';
import lib from './rollup.lib.config';

export default [...lib, ...base];
  • Add script into package.json
...
    "dev": "rollup --config ./scripts/rollup.all.config.js --environment NODE_ENV:development --watch",
    "build": "rollup --config ./scripts/rollup.all.config.js --environment NODE_ENV:production",
...

Be the first to comment

Leave a Reply

Your email address will not be published.


*