React Module Federation allows independent React applications to share components and code at runtime, effectively treating them as a single, cohesive application.
Let’s see it in action. Imagine you have two separate React apps: a "Host" app and a "Remote" app. The Host app wants to use a button component that’s defined and built within the Remote app.
Here’s a simplified webpack.config.js for the Remote app (the one providing the component):
// remote/webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
entry: './src/index',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
publicPath: 'http://localhost:3001/', // Important for runtime loading
},
mode: 'development',
devServer: {
port: 3001,
headers: {
"Access-Control-Allow-Origin": "*", // CORS headers for sharing
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
},
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
new ModuleFederationPlugin({
name: 'remoteApp', // The name of this remote module
filename: 'remoteEntry.js', // The entry point file for this remote
exposes: {
'./Button': './src/components/Button', // Expose './Button' from './src/components/Button'
},
shared: {
react: { singleton: true, eager: true }, // Share React
'react-dom': { singleton: true, eager: true }, // Share React DOM
},
}),
],
};
And here’s the webpack.config.js for the Host app (the one consuming the component):
// host/webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
entry: './src/index',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
publicPath: 'http://localhost:3000/',
},
mode: 'development',
devServer: {
port: 3000,
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
new ModuleFederationPlugin({
name: 'hostApp',
remotes: {
remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js', // Map 'remoteApp' to its entry point
},
shared: {
react: { singleton: true, eager: true }, // Share React
'react-dom': { singleton: true, eager: true }, // Share React DOM
},
}),
],
};
Now, in the Host app’s src/App.js, you can dynamically import the shared component:
// host/src/App.js
import React, { Suspense } from 'react';
const Button = React.lazy(() => import('remoteApp/Button')); // Import from the remote
function App() {
return (
<div>
<h1>Host App</h1>
<Suspense fallback={<div>Loading...</div>}>
<Button onClick={() => alert('Clicked from Host!')}>
Shared Button
</Button>
</Suspense>
</div>
);
}
export default App;
When the Host app runs, it starts its own Webpack build. The ModuleFederationPlugin configures it to know about remoteApp at http://localhost:3001/remoteEntry.js. When React.lazy(() => import('remoteApp/Button')) is encountered, the Host app’s runtime will make a network request to http://localhost:3001/remoteEntry.js. This file contains the metadata about remoteApp, including how to load the actual Button component. The Host then dynamically fetches and executes the code for the Button component, making it available for rendering.
The core problem Module Federation solves is the "dependency hell" and code duplication that arises when multiple applications need to use the same libraries or components. Instead of each app bundling its own copy of React or a shared design system, Module Federation allows them to agree on a single version loaded by one of the applications (or a dedicated shared runtime) and share it. This drastically reduces bundle sizes and ensures consistency.
Internally, Webpack’s ModuleFederationPlugin works by generating "remote entry" files. These files act as a manifest for the federated modules. When a host application needs a module from a remote, it first requests the remote’s entry file. This entry file contains information about how to locate and load the requested module, often involving asynchronous loading of separate JavaScript chunks. The shared configuration is crucial: it tells Webpack which dependencies should be treated as shared. If a shared dependency is found in the host, it’s used; otherwise, it’s loaded from the remote. The singleton: true option ensures that only one version of a shared dependency is ever loaded across all federating applications, preventing multiple copies of React from being bundled. eager: true forces the shared dependency to be loaded immediately, rather than on demand.
A common point of confusion is how publicPath interacts with Module Federation. The publicPath in the output configuration of both the remote and the host is critical. For the remote, it tells the browser where to find its own chunks and the remoteEntry.js file when requested by the host. For the host, it dictates where its own assets are served from, but more importantly, it influences how the remote’s remoteEntry.js URL is constructed if it’s relative. Always ensure your publicPath is correctly set to a domain or CDN where the built assets will be served.
The way shared dependencies are resolved is often misunderstood. If a shared dependency (like react) is listed in both the host and the remote, Webpack will prioritize the version already loaded by the host. If the host doesn’t have it, it will try to load it from the remote. This negotiation is key to achieving true runtime sharing and avoiding duplicate bundles.
When you successfully share a component, the next challenge you’ll likely face is managing state that needs to be shared across these independently deployed applications.