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", ...
Leave a Reply