Beginner’s Guide to Module Federation

Abhishek Kedia
6 min readMar 11, 2024

--

Module Federation is an advanced feature in Webpack that provides a way for a JavaScript application to dynamically load code from another application. This feature allows for efficient code sharing and dependency management.
It allows you to build Micro-Frontends (MFEs). A micro-frontend is a development approach that breaks down a web application’s front end into smaller, self-contained modules. Each module can be developed, tested, and deployed independently.
This allows you to integrate multiple smaller applications into a single application (called the host app or the shell app). The modules are loaded dynamically on demand.

Let’s get the basics right

Module Federation has three main components:

  1. Exposed Module (Remote App)
  2. Host Module (Shell App)
  3. Shared dependencies

Example:

Remote application exposes a component with react as shared dependency:

// Webpack file content of the remote app
new ModuleFederationPlugin({
name: 'remoteApp',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/Button.jsx',
},
shared: {
'react': {
version: '18.2.0'
}
}
})

Host application receives the remote app:

// Webpack file content of the host app
new ModuleFederationPlugin({
name: 'host',
remotes: {
'remoteApp': 'remoteApp@http://localhost:3001/remoteEntry.js',
},
shared: ['react'],
})

Rendering the remote component in the host (dynamically):

// App.js in the host app
import React from 'react';
import RemoteAppButton = React.lazy(async () =>
await import('remoteApp/Button'));

const App = () => {
return (
<React.Suspense fallback="Loading">
<RemoteAppButton /> // You can pass props like a normal component
</React.Suspense>
);
}

Sharing Libraries

With the shared key in the configuration you can define libraries that are shared between your federated modules. The package name is the same as the one found in the dependencies section of the package.json.

Dependencies can be shared from both the host application and the remote application in a module federation setup, depending on how you configure it.
Host Application Sharing Dependencies: These shared dependencies will be included in the host’s bundle, and when a remote application requests them, they will be served from the host’s bundle. This allows the host application to control which dependencies are shared across the entire application.
Remote Application Sharing Dependencies: These dependencies are shared with the host application and other remote applications. These shared dependencies will be included in the remote’s bundle, and when the host or other remotes request them, they will be served from the remote’s bundle.

Why do we want to share libraries?

When you have federated modules, they’re bundled separately and include all the dependencies they need to function. However, when they’re used in a host application, it’s possible for multiple copies of the same dependency to be downloaded. This can hurt performance and make users download more JavaScript than necessary. 😰
This is where the shared API becomes really handy: You can avoid downloading multiple copies of the same dependency and improve the performance of your application. 🚀

Example

Let’s say you have two modules, Module A and Module B, and both of them require lodash in order to function independently.
But when they’re used in a host application that brings both modules together, the Shared API comes into play. If there is already a preloaded, shared copy of lodash available, Module A and Module B will use that copy instead of loading their own independent copies.
This copy could be loaded by made available by the host or another remote application inside it.

new ModuleFederationPlugin({
...
shared: ["lodash"],
});

API Definition

shared (object | [string]): An object or Array of strings containing a list of dependencies that can be shared and consumed by other federated apps.

eager (boolean): If true, the dependency will be eagerly loaded and made available to other federated apps as soon as the host application starts. If false, the dependency will be lazily loaded when it is first requested by a federated app.

singleton (boolean): If true, the dependency will be treated as a singleton and only a single instance of it will be shared among all federated apps.

requiredVersion (string): Specifies the required version of the dependency. If a federated app tries to load an incompatible version of the dependency, two copies will be loaded.

shared: {
react: {
singleton: true,
requiredVersion: "^18.0.0",
},
"react-dom": {
singleton: true,
requiredVersion: "^18.0.0"
},
lodash: {
eager: true,
},
},

Versioning

So what happens if two remote modules use different versions of the same dependency?
Nothing, everything works! If the semantic version ranges for those dependencies don’t match, then Module Federation is powerful enough to identify them and provide separate copies. It’s just that module federation tries to download all the different versions, and overall bundle size increases.

Singleton Loading

Passing singleton: true to the dependency object, ensures only that specific version of the dependency is downloaded.

Common Errors

When a module is required, it will load a file called remoteEntry.js listing all the dependencies the module needs. Because this operation is asynchronous, the container can check all the remoteEntry files and list all the dependencies that each module has declared in shared; then, the host can load a single copy and share it with all the modules that need it.

Because shared relies on an asynchronous operation to be able to inspect and resolve the dependencies, if your application or module loads synchronously and it declares a dependency in shared, you might encounter the following error:

Uncaught Error: Shared module is not available for eager consumption

To solve the error above, there are two options:

Eager Consumption

Individual dependencies can be marked as eager: true; this option doesn’t put the dependencies in an async chunk, so they can be provided synchronously; however, this means that those dependencies will always be downloaded, potentially impacting bundle size. The recommended solution is to load your module asynchronously by wrapping it into an async boundary:

Using Async Boundary

To create an async boundary, use a dynamic import to ensure your entry point runs asynchronously:

// index.js
import('./bootstrap.js');
// bootstrap.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));

Importance of publicPath

The public path in webpack configuration specifies the base URL for all assets within your application. It’s crucial for resolving asset paths correctly, especially in applications with dynamic loading or code splitting.
In applications that use dynamic imports or code splitting, the public path determines the base URL for dynamically loaded chunks. This is essential for ensuring that webpack can load these chunks correctly, regardless of where they’re requested from in the application.

Public Path 'auto':

When you set publicPath to 'auto', webpack will try to determine the correct public path based on where your HTML file is hosted. It essentially infers the public path dynamically based on the URL of the HTML file that loads the webpack bundle.
In a module federation setup, when the host serves assets to remote applications, it will use the dynamically determined public path based on 'auto'. This ensures that assets are loaded correctly relative to the base URL of the HTML file, regardless of the deployment environment.

output: {
publicPath: 'auto', // this is for Module Federation exposition
},
...
plugins[
new HtmlWebpackPlugin({
publicPath: '/', // this is for standalone mode
...
],

Let’s Get in Touch

You are most welcome to follow me here on Medium. In addition, feel free to check out:

References

https://webpack.js.org/concepts/module-federation/
https://www.infoxicator.com/en/module-federation-shared-api
https://scriptedalchemy.medium.com/understanding-webpack-module-federation-a-deep-dive-efe5c55bf366

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

No responses yet

Write a response