Webpack 5 Module Federation

Raju
Devjam
Published in
13 min readAug 14, 2023

Introduction to Webpack 5 Module Federation

Webpack 5 Module Federations is a new feature introduced in the latest version of Webpack — Webpack 5. This feature allows developers to share modules across multiple projects, without the need for a central registry.

The main advantage of using Webpack 5 Module Federations is that it enables developers to create micro-frontends, which are self-contained, independent parts of a larger application. This means that developers can build and deploy smaller, more modular applications, which can be updated and maintained independently of each other.

It’s important to note that although this guide uses ReactJS as an example, Webpack 5 Module Federation is not limited to any specific JavaScript framework. The concepts and techniques outlined here can be easily adapted to work with other frameworks, such as Lit-Elements or Vue.js.

Another benefit of using Webpack 5 Module Federations is that it can significantly reduce the complexity of managing dependencies between different projects. With this feature, developers can easily share and reuse code between different projects, without worrying about version conflicts or compatibility issues.

Overall, Webpack 5 Module Federations is a powerful new feature that can help developers build more modular, scalable, and maintainable applications. If you’re working on a large-scale project or building micro-frontends, then this feature is definitely worth exploring.

About this guide

In this guide, we’ll walk you through the process of using Webpack 5 Module Federations to create a set of self-contained micro-frontends. We’ll demonstrate how to build and deploy these micro-frontends as standalone applications, and then show you how to combine them into a larger application. Along the way, we’ll cover topics such as configuring Webpack 5 for module federation, defining and exposing remote modules, and consuming remote modules from a host application. By the end of this guide, you should have a solid understanding of how to use Webpack 5 Module Federations to build modular, scalable, and maintainable applications.

In the following section, the repositories that will be created include “UI Components”, “Home page”, “Signup page” and a “Host Application”. The section will provide step-by-step instructions for setting up each repository and configuring them to work together using Webpack 5 Module Federation.

To begin, the UI Components Repository will contain all of the shared UI components that will be used across the different micro-frontends.

The Homepage and Signup page Repositories will each contain a single page component that will be used to render a specific page in our application. The steps to set up these repositories are similar to those for the UI Components Repository.

The Host Application Repository will contain the main application code that will import and use the micro-frontends from the other repositories.

By following these steps, the necessary repositories will be set up and configured to work together using Webpack 5 Module Federations. Next, we’ll take a closer look at how to define and export micro-frontends from each repository.

You can view more on the supporting repositories and live demos:

  • UI components — A library of reusable UI components — Github repo.
  • Homepage — A sample page that uses the UI components to display some content — Github repo.
  • Signup page — Another sample page that uses the UI components and some additional custom code to display more content — Github repo.
  • Host application — The main application that combines the micro-frontends and renders them in a unified way — Github repo.

Setting Up the Repositories

To create micro-frontends using Webpack 5 Module Federations, we will need to set up several repositories. In this section, we’ll walk through the process of creating each repository and configuring them to work together. The repositories we’ll create are:

  • UI Components
  • Homepage Repository
  • Signup page Repository
  • Host Application Repository

Create a module federation base

1 — Create a new directory for your repository and initialize a new Node.js project in that directory using npm init.

2 — Install the required dependencies for your project.

npm install webpack webpack-cli html-webpack-plugin webpack-dev-server --save-dev
npm install react react-dom
npm install @babel/core @babel/preset-react babel-loader --save-dev

3 — Create a new index.html file in your project directory. This file will be used as the template for the generated HTML file.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>App</title>
</head>
<body>
<div id="app"></div>
<script type="text/javascript" src="index.js"></script>
</body>
</html>

4 — Now create a index.js file that will load the bootstrap file.

import("./bootstrap");

5 — Create a bootstrap.js file in the root directory. This will be the file that is run locally to demo the UI components.

(note: bootstrap.js is needed to prevent an error that occurs when an omnidirectional host eagerly executes an application without waiting for the shared module to load. To solve this, you can use bootstrap.js to import necessary modules asynchronously or set eager: true for the dependency via ModuleFederationPlugin. See here for more.)

import React from 'react';
import ReactDom from 'react-dom';

const App = () => {
return (
<div>
<h1>Hello world</h1>
</div>
);
};

ReactDom.render(<App />, document.getElementById('app'));

6 — Create a new webpack.config.js file in your project directory. This file will contain the configuration for your Webpack build.

const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
mode: 'development',
entry: './index.js',
output: {
filename: 'main.js',
path: __dirname + '/dist',
},
module: {
rules: [
{
test: /\\.jsx?$/,
loader: "babel-loader",
exclude: /node_modules/,
options: {
presets: ["@babel/preset-react"],
},
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './index.html',
}),
new ModuleFederationPlugin({
name: 'uicomponents',
filename: 'remoteEntry.js',
exposes: {
// './Button': './src/Button.js',
},
shared: {react: {singleton: true}, "react-dom": {singleton: true}}
}),
],
};

we now have a module federation base we can modify in the following examples as we create an application of using webpack 5 module federation

UI Components Repository

The UI Components Repository will contain all of the shared UI components that will be used across the different micro-frontends. To set up this repository:

1 — Using the base created in the previous section “Create a module federation base”, we start adding components that we want to expose.

2 — Create a new file src/Button.js in your project directory. This file will contain the code for your exported button component.

import React from 'react';

const Button = ({ children }) => {
return <button>{children}</button>;
};

export default Button;

3 — Update the webpack.config.js file in your project directory (as described in “Create a module federation base” step 4). we will update for it to expose the Button component.

new ModuleFederationPlugin({
name: 'uicomponents',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/Button.js', // << exposing `Button`
},
shared: {react: {singleton: true}, "react-dom": {singleton: true}}
})

4 — Update the bootstrap.js file in the root directory to use the button component. This is the same as setting up any reactjs app.

import React from 'react';
import ReactDom from 'react-dom';
import Button from './src/Button';

const App = () => {
return (
<div>
<h1>Module Federation</h1>
<hr />
Button component: <Button>Click me</Button>
</div>
);
};

ReactDom.render(<App />, document.getElementById('app'));

5 — Run your build using webpack and start a local server to view your exported button component in action!

npx webpack serve

Your application should now be running and display a demo for the Button component. you can see an example of this repository here.

Homepage and Signup Repositories

The Homepage and signup Repositories will each contain a single page component that will be used to render a specific page in our application. To set up these repositories the steps will be similar. So let’s start off with the homepage:

1 — Using the base created in the previous section “Create a module federation base”, we start adding components that we want to expose.

2 — we will be using react-router-dom for navigating in our app. so lets add that dependency with:

npm install react-router-dom

3 — Create a new file src/Homepage.js in your project directory. This file will contain the code for your exported Homepage component.

import React from 'react';
import Button from 'uicomponents/Button';
import { Link } from 'react-router-dom';

const Homepage = ({ children }) => {
return (
<>
<h1>Module Federation with Webpack 5</h1>
<p>
Module Federation is a new feature in Webpack 5 that allows you to
dynamically load code from other webpack bundles at runtime.
</p>
<Button onClick={() => alert('Button component from federated ui components library')}>Click me</Button>
<br />
<Link to="/signup">Signup</Link>
</>
);
};

export default Homepage;

(Note: Here you see that a button component is imported from a federated module. We will go through how to get that to work in the following steps.)

4 — Update the webpack.config.js file in your project directory (as described in “Create a module federation base” step 4). we will update for it to expose the Homepage component and add uicomponents as a federated dependency.

new ModuleFederationPlugin({
name: 'homepage',
filename: 'remoteEntry.js',
exposes: {
'./Homepage': './src/Homepage.js',
},
remotes: {
uicomponents: "uicomponents@<https://sytac-webpack-module-federation.github.io/ui-components/remoteEntry.js>",
},
shared: {
react: {singleton: true},
"react-dom": {singleton: true},
"react-router-dom": {singleton: true}
}
})

5 — Update the bootstrap.js file in the root directory to use the button component. This is the same as setting up any reactjs app.

import React from 'react';
import ReactDom from 'react-dom';
import { createHashRouter, RouterProvider } from 'react-router-dom';
import Homepage from './src/Homepage';

const App = () => {
const router = createHashRouter([
{
path: "/",
element: <Homepage />,
},
]);
return (
<RouterProvider router={router} />
);
};

ReactDom.render(<App />, document.getElementById('app'));

(Note: When using Link from react-router-dom, the RouterProvider has to be in context to prevent errors from being thrown. Here is a basic example of a router with 1 route.)

6 — Run your build using webpack and start a local server to view your exported page component in action!

npx webpack serve

Your application should now be running and display a demo for the Homepage component. You can see an example of this repository.

The Signup component is created similarly and can be seen in the repository.

Host Application Repository

Finally we have the Host Application Repository. this contains the main application code that will import and use the micro-frontends from the other repositories. To set up this repository:

1 — Using the base created in the previous section “Create a module federation base”, we start adding components that we want to expose.

2 — we will be using react-router-dom for navigating in our app. so lets add that dependency with:

npm install react-router-dom

3 — Create a new file src/MainApp.js in your project directory. This file will contain the code for your exported component.

import React from 'react';
import Signup from 'signup/Signup';
import Homepage from 'homepage/Homepage';
import {
RouterProvider,
createHashRouter
} from "react-router-dom";

const MainApp = ({ children }) => {
const router = createHashRouter([
{
path: "/",
element: <Homepage />,
},
{
path: "/signup",
element: <Signup />,
},
]);
return (
<>
<RouterProvider router={router} />
</>
);
};

export default MainApp;

4 — Update the webpack.config.js file in your project directory (as described in “Create a module federation base” step 4). we will update for it to add Homepage and Signup as federated dependencies.

new ModuleFederationPlugin({
name: 'website',
filename: 'remoteEntry.js',
remotes: {
homepage: "homepage@<https://sytac-webpack-module-federation.github.io/homepage/remoteEntry.js>",
signup: "signup@<https://sytac-webpack-module-federation.github.io/signup/remoteEntry.js>",
},
shared: {
react: {singleton: true},
"react-dom": {singleton: true},
"react-router-dom": {singleton: true}
}
})

5 — Update the bootstrap.js file in the root directory to use the button component. This is the same as setting up any reactjs app.

import React from 'react';
import ReactDom from 'react-dom';
import MainApp from './src/MainApp';

const App = () => {
return (
<MainApp />
);
};

ReactDom.render(<App />, document.getElementById('app'));

6 — Run your build using webpack and start a local server to view your application in action.

npx webpack serve

Your application should now be running and display a demo for the App. You can see an example of this repository.

Combining the Micro-Frontends

Now that we’ve created the individual micro-frontends, let’s take a look at how they work together to create a complete application. Below is a table that shows the different micro-frontends we’ve created in this guide, along with a brief description of what they do and a link to their source code:

  • UI components — A library of reusable UI components — Github repo.
  • Homepage — A sample page that uses the UI components to display some content — Github repo.
  • Signup page — Another sample page that uses the UI components and some additional custom code to display more content — Github repo.
  • Host application — The main application that combines the micro-frontends and renders them in a unified way — Github repo.

By using Webpack 5 Module Federations to share and consume modules across these different micro-frontends, we’ve been able to build a complete application that is modular, scalable, and maintainable. This approach has many benefits, including making it easier to manage dependencies between different projects, reducing the complexity of large-scale applications, and enabling teams to work on independent parts of the application without interfering with each other.

Framework/Dependency agnostic

One of the key advantages of Webpack 5 Module Federations is its framework-agnostic nature. While Webpack is often associated with JavaScript frameworks like React, Angular, or Vue.js, it can be used with any front-end framework or library.

To illustrate this, let’s take a look at an example of a LitElement button component. LitElement is a lightweight web component library that provides a simple way to create reusable components using modern web standards.

Here’s an example of a LitElement button component:

import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';

export class MyButton extends LitElement {
static properties = {
title: { type: String },
};
render() {
return html`
<button>${this.title}</button>
`;
}
}

export default customElement('my-button')(MyButton);

In the Webpack configuration, we can export both the React button component and the LitElement button component:

new ModuleFederationPlugin({
name: 'uicomponents',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/Button.js',
'./LitButton': './src/LitButton.js',
},
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
}),

When using Webpack 5 Module Federations, the exported LitElement button component will be bundled and sent along with its dependencies because LitElement is not a shared dependency. This ensures that the component can be used independently in the consuming application.

In the consuming application, we can import the LitElement button component alongside other components:

import React from 'react';
import 'uicomponents/LitButton';

const Signup = () => {
return (
<>
<h1>Sign up now</h1>
<p>This is a Lit Button</p>
<my-button title="Submit"></my-button>
</>
);
};

export default Signup;

As shown in the example, the LitElement button component can be seamlessly imported and used in a React application without any conflicts. This demonstrates the framework-agnostic nature of Webpack 5 Module Federations, enabling the composition of components from different frameworks within the same application.

Webpack 5’s ability to handle different frameworks and libraries makes it a versatile choice for module federation, allowing teams to leverage the benefits of micro-frontends regardless of their preferred front-end technology stack.

Lazy loading components

In the previous chapter, we discussed how Webpack 5 Module Federations can be used with different frameworks, including LitElement alongside React. However, when exporting the LitElement button component, we didn’t declare Lit as a shared dependency. As a result, when loading the application, the additional size added for the LitElement dependency is also loaded.

To optimize the loading of components and reduce the initial bundle size, we can take advantage of React’s lazy loading feature. Lazy loading allows us to load components asynchronously, meaning that the component and its dependencies are only loaded when they are actually needed.

In the host application, we can use React’s lazy function to lazily load the Signup component:

const Signup = lazy(() => import('signup/Signup'));

By wrapping the import statement with lazy, the Signup component will be dynamically loaded when it is first rendered.

Next, in the router configuration, we can use the Suspense component to wrap the dynamically imported Signup component:

const router = createHashRouter([
{
path: "/",
element: <Homepage />,
},
{
path: "/signup",
element: (
<Suspense fallback={<Spinner />}>
<Signup />
</Suspense>
),
},
]);

The Suspense component allows us to show a fallback UI, such as a loading spinner, while the lazy-loaded component is being fetched. Once the component is loaded, it will be rendered in place of the fallback UI.

By applying lazy loading and using Suspense, we can optimize the initial loading time of the application and improve the user experience. The Signup component, along with its dependencies, will be loaded only when the user navigates to the corresponding route.

This approach can be extended to lazy load other components within the microfrontend architecture, including the LitElement button component. In the Signup component, we can use lazy again to lazily load the LitButton component:

const LitButton = lazy(() => import('uicomponents/LitButton'));

By leveraging lazy loading at multiple levels, we can achieve a more granular loading strategy, where each component is loaded on-demand, minimizing the initial bundle size and optimizing performance.

Conclusion

In this article, we have introduced you to Webpack 5 Module Federations and walked through the process of creating self-contained micro-frontends using this feature. By following the steps outlined in this guide, you should have a solid understanding of how to use Webpack 5 Module Federations to share and consume modules across multiple projects.

Check out the live demo hosted on github pages.

Benefits, advantages, further reading

Here are some key benefits and advantages of using Webpack 5 Module Federations:

  • Enables developers to create micro-frontends that are self-contained, independent parts of a larger application
  • Allows developers to build and deploy smaller, more modular applications that can be updated and maintained independently of each other
  • Significantly reduces the complexity of managing dependencies between different projects
  • Enables easy sharing and reusing of code between different projects, without worrying about version conflicts or compatibility issues (a nice article about version resolution)
  • Helps to build more modular, scalable, and maintainable applications

Webpack 5 Module Federations is a powerful feature that can help you build better applications with more efficient workflows. We encourage you to explore this feature and experiment with it in your own projects. If you’re interested in learning more, check out the official Webpack documentation for more detailed information.

--

--