How to Use Webpack Module Federation in React
May 9, 2022 ☕ 8 min readModule Federation is an excellent tool for constructing a micro-frontend architecture in React applications. I will show you how to use it in a step-by-step guide to building a Host-Remote pattern micro-frontend in React.
Why a Micro Frontend?
Micro-frontends help us break large frontend applications into smaller independent applications or modules that can be built and deployed at their cadence.
Doing this using Module Federation allows us to combine the applications at run time in the client’s browsers and eliminate build-time dependencies and coordination, allowing the teams building these applications to develop at scale.
Getting started
You can follow along with the final code found here: https://github.com/rautio/react-micro-frontend-example
We are building two applications: host
and remote
.
The host
app is the "main" app and remote
is a sub-app plugging into it.
Module Federation does support treating the host
as a remote and making the architecture peer-to-peer if it fits your use case. More on this later.
We're going to use create-react-app
to simplify the initial steps.
In your root directory:
npx create-react-app hostnpx create-react-app remote
this will create two apps for you:
host/remote/
Dependencies
Within each host/
and remote/
run:
npm install --save-dev webpack webpack-cli html-webpack-plugin webpack-dev-server babel-loader
This will install wepback and the dependencies we need for our webpack configuration.
Webpack Module Federation is only available in version 5 and above of webpack.
Host App
webpack.config.js
We are going to start with our webpack configuration.
Create a new webpack.config.js
file at the root of host/
:
// host/webpack.config.jsconst HtmlWebpackPlugin = require("html-webpack-plugin");module.exports = {entry: "./src/index",mode: "development",devServer: {port: 3000,},module: {rules: [{test: /\.(js|jsx)?$/,exclude: /node_modules/,use: [{loader: "babel-loader",options: {presets: ["@babel/preset-env", "@babel/preset-react"],},},],},],},plugins: [new HtmlWebpackPlugin({template: "./public/index.html",}),],resolve: {extensions: [".js", ".jsx"],},target: "web",};
This is a basic webpack example to get our js
and jsx
code transpiled using babel-loader
and injected into an html
template.
Update package.json scripts
Next we need a new start
script that utilizes our webpack config:
"scripts":{"start": "webpack serve"}
Now we can get to the meat of the host app.
index.js
First, we need the index.js
entry to our app. We are importing another file bootstrap.js
that renders the React app.
We need this extra layer of indirection because it gives Webpack a chance to load all of the imports it needs to render the remote app.
Otherwise, you would see an error along the lines of:
Shared module is not available for eager consumption
// host/src/index.jsimport("./bootstrap");// Note: It is important to import bootstrap dynamically using import() otherwise you will also see the same error.
bootstrap.js
Next, we define the bootstrap.js
file that renders our React application.
// host/src/bootstrap.jsimport React from "react";import ReactDOM from "react-dom/client";import App from "./App";const root = ReactDOM.createRoot(document.getElementById("root"));root.render(<React.StrictMode><App /></React.StrictMode>);
App.js
Now we are ready to write our App.js
file where the app’s main logic happens. Here we will load two components from remote
which we will define later.
import("Remote/App")
will dynamically fetch the Remote app’s App.js
React component.
We need to use a lazy loader and an ErrorBoundary component to create a smooth experience for users in case the fetching takes a long time or introduces errors in our host app.
// host/src/App.jsimport React from "react";import ErrorBoundary from "./ErrorBoundary";const RemoteApp = React.lazy(() => import("Remote/App"));const RemoteButton = React.lazy(() => import("Remote/Button"));const RemoteWrapper = ({ children }) => (<divstyle={{border: "1px solid red",background: "white",}}><ErrorBoundary>{children}</ErrorBoundary></div>);export const App = () => (<div style={{ background: "rgba(43, 192, 219, 0.3)" }}><h1>This is the Host!</h1><h2>Remote App:</h2><RemoteWrapper><RemoteApp /></RemoteWrapper><h2>Remote Button:</h2><RemoteWrapper><RemoteButton /></RemoteWrapper><br /><a href="http://localhost:4000">Link to Remote App</a></div>);export default App;
Add Module Federation
We're not ready to run the app just yet. Next, we need to add Module Federation to tell our host
where to get the Remote/App
and Remote/Button
components.
In our webpack.config.js
we introduce the ModuleFederationPlugin
:
// host/webpack.config.jsconst ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");const { dependencies } = require("./package.json");module.exports = {//...plugins: [new ModuleFederationPlugin({name: "Host",remotes: {Remote: `Remote@http://localhost:4000/moduleEntry.js`,},shared: {...dependencies,react: {singleton: true,requiredVersion: dependencies["react"],},"react-dom": {singleton: true,requiredVersion: dependencies["react-dom"],},},}),//...],//...};
Important things to note:
name
is used to distinguish the modules. It is not as important here because we are not exposing anything, but it is vital in theRemote
app.remotes
is where we define the federated modules we want to consume in this app. You'll notice we specifyRemote
as the internal name so we can load the components usingimport("Remote/<component>")
. But we also define the location where the remote's module definition is hosted:Remote@http://localhost:4000/moduleEntry.js
. This URL tells us three important things. The module's name isRemote
, it is hosted onlocalhost:4000
and its module definition ismoduleEntry.js
.shared
is how we share dependencies between modules. This is very important for React because it has a global state, meaning you should only ever run one instance of React and ReactDOM in any given app. To achieve this in our architecture, we are telling webpack to treat React and ReactDOM as singletons, so the first version loaded from any modules is used for the entire app. As long as it satisfies therequiredVersion
we define. We are also importing all of our other dependencies frompackage.json
and including them here, so we minimize the number of duplicate dependencies between our modules.
Now, if we run npm start
in the host app we should see something like:
This means our host
app is configured, but our remote
app is not exposing anything yet. So we need to configure that next.
Remote App
webpack.config.js
Let’s start with the webpack config. Since we now have some knowledge of Module Federation, let’s use it from the get-go:
// remote/webpack.config.jsconst HtmlWebpackPlugin = require("html-webpack-plugin");const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");const path = require("path");const { dependencies } = require("./package.json");module.exports = {entry: "./src/index",mode: "development",devServer: {static: {directory: path.join(__dirname, "public"),},port: 4000,},module: {rules: [{test: /\.(js|jsx)?$/,exclude: /node_modules/,use: [{loader: "babel-loader",options: {presets: ["@babel/preset-env", "@babel/preset-react"],},},],},],},plugins: [new ModuleFederationPlugin({name: "Remote",filename: "moduleEntry.js",exposes: {"./App": "./src/App","./Button": "./src/Button",},shared: {...dependencies,react: {singleton: true,requiredVersion: dependencies["react"],},"react-dom": {singleton: true,requiredVersion: dependencies["react-dom"],},},}),new HtmlWebpackPlugin({template: "./public/index.html",}),],resolve: {extensions: [".js", ".jsx"],},target: "web",};
The important things to note are:
- Our webpack dev server runs at
localhost:4000
- The remote module's name is
Remote
- The
filename
ismoduleEntry.js
Combined these together will allow our host to find the remote code at Remote@http://localhost:4000/moduleEntry.js
exposes
is where we define the code we want to share in the moduleEntry.js
file. Here we are exposing two: <App />
and <Button />
.
Now let’s set up those components and our Remote app so it can run independently.
index.js
Similar to the host app, we need a dynamic import in our webpack entry.
// /remote/src/index.jsimport("./bootstrap");
bootstrap.js
// remote/src/bootstrap.jsimport React from "react";import ReactDOM from "react-dom/client";import App from "./App";const root = ReactDOM.createRoot(document.getElementById("root"));root.render(<React.StrictMode><App /></React.StrictMode>);
App.js
The Remote app is much simpler than the Host:
// remote/src/App.jsimport React from "react";export const App = () => {return <div>Hello from the other side</div>;};export default App;
Button.js
And we also want to expose a <Button /> component
// remote/src/Button.jsimport React from "react";export const Button = () => <button>Hello!</button>;export default Button;
Now the Remote app is fully configured and if you run npm start
you should see a blank page with "Hello from the other side."
Putting it all together
Now if we run npm start
in both the host/
and remote/
directories we should see the Host app running on localhost:3000
and the remote app running on localhost:4000
.
The host app would look something like:
Congratulations! You've now configured a Micro Frontend app using React.
Follow ups
Development
You can simplify the development flow by configuring yarn workspaces at the root level: https://classic.yarnpkg.com/lang/en/docs/workspaces/
Deployment
We only covered running the Micro Frontend locally. If you want to deploy them, you would deploy each app separately to their CDN or hosting service and configure the webpack definitions to use environment variables or some other way to update the URLs in the ModuleFederationPlugin
definitions.
You can find an example of this in my more advanced example app: https://github.com/rautio/micro-frontend-demo
Resources
Code for this example: https://github.com/rautio/react-micro-frontend-example
A more advanced example: https://github.com/rautio/micro-frontend-demo
Webpack's example app for peer-to-peer structure: https://stackblitz.com/github/webpack/webpack.js.org/tree/master/examples/module-federation?terminal=start&terminal=
Webpack docs: https://webpack.js.org/concepts/module-federation/