Seamlessly Unifying Diverse Frontend Applications with Module Federation
Ethan Miller
Product Engineer · Leapcell

Introduction
In the rapidly evolving landscape of modern web development, particularly within large organizations or complex product suites, the monolithic frontend architecture often presents significant challenges. Teams struggle with slow build times, difficulty in deploying independent features, and the ever-present risk of tightly coupled deployments. As applications grow in complexity and feature sets, the need for a more modular, scalable, and independently deployable approach becomes increasingly critical. This is where the concept of composing multiple independent frontend applications into a single, unified user experience comes into play. This article will delve into Module Federation, a powerful feature that addresses these very challenges, enabling developers to build sophisticated systems by seamlessly integrating distinct frontend applications.
Understanding the Core Concepts of Module Federation
Before we dive into the mechanics and benefits of Module Federation, it's essential to grasp a few core concepts that underpin its operation.
- Host (or Container) Application: This is the main application that consumes and orchestrates modules from other remote applications. It acts as the shell or wrapper.
- Remote Application: These are the independent applications that expose specific modules or components to be consumed by other host or remote applications.
- Exposed Modules: These are the specific pieces of code (components, functions, utilities, etc.) that a remote application chooses to make publicly available for other applications to use.
- Shared Modules: Module Federation inherently supports sharing dependencies among applications. This is crucial for optimizing bundle sizes and ensuring only one version of a shared library (like React or Vue) is loaded at runtime, preventing potential conflicts and bloat.
How Module Federation Works
Module Federation, introduced with Webpack 5, allows a JavaScript application to dynamically load code from another application and share dependencies in a robust and efficient way. At its core, Module Federation enables a "host" application to load "remote" modules at runtime.
Consider a scenario where you have an e-commerce platform. Instead of building the entire platform as one massive frontend, you could separate concerns into independent micro-frontends: a "Product Catalog" application, a "Shopping Cart" application, and a "User Profile" application. With Module Federation, the "Product Catalog" might expose a FeaturedProduct
component, the "Shopping Cart" an AddToCartButton
, and the "User Profile" an AuthWidget
. A main "Shell" application then acts as the host, pulling in and rendering these components as needed.
Let's illustrate with a simple example.
Setting up a Remote Application (Exposing Components)
First, we define our remote application, let's call it product-catalog
. Its goal is to expose a ProductCard
component.
// product-catalog/src/components/ProductCard.jsx import React from 'react'; const ProductCard = ({ name, price }) => { return ( <div style={{ border: '1px solid #ccc', padding: '10px', margin: '10px' }}> <h3>{name}</h3> <p>Price: ${price}</p> <button>View Details</button> </div> ); }; export default ProductCard;
Now, we configure webpack.config.js
for product-catalog
to expose this component:
// product-catalog/webpack.config.js const HtmlWebpackPlugin = require('html-webpack-plugin'); const { ModuleFederationPlugin } = require('webpack').container; module.exports = { mode: 'development', entry: './src/index.js', output: { publicPath: 'http://localhost:3001/', // Public path for the exposed modules }, devServer: { port: 3001, }, module: { rules: [ { test: /\.jsx?$/, loader: 'babel-loader', exclude: /node_modules/, options: { presets: ['@babel/preset-react'], }, }, ], }, plugins: [ new ModuleFederationPlugin({ name: 'product_catalog', // Unique name for this remote application filename: 'remoteEntry.js', // The file that contains exposed modules metadata exposes: { './ProductCard': './src/components/ProductCard', // Expose ProductCard component }, shared: { react: { singleton: true, requiredVersion: '^18.0.0' }, 'react-dom': { singleton: true, requiredVersion: '^18.0.0' }, }, }), new HtmlWebpackPlugin({ template: './public/index.html', }), ], };
Here, name
is the unique identifier for this remote. filename
is the manifest file that the host will fetch. exposes
defines what modules are available. shared
is crucial for dependency de-duplication.
Setting up a Host Application (Consuming Components)
Next, we create our host application, main-shell
, which will consume ProductCard
from product-catalog
.
// main-shell/src/App.jsx import React, { Suspense } from 'react'; // Dynamically import the remote component const ProductCard = React.lazy(() => import('product_catalog/ProductCard')); const App = () => { return ( <div> <h1>Welcome to the Main Shell!</h1> <h2>Featured Products:</h2> <Suspense fallback={<div>Loading Product Card...</div>}> <ProductCard name="Stylish Headphones" price="199.99" /> <ProductCard name="Ergonomic Keyboard" price="120.00" /> </Suspense> </div> ); }; export default App;
And its webpack.config.js
:
// main-shell/webpack.config.js const HtmlWebpackPlugin = require('html-webpack-plugin'); const { ModuleFederationPlugin } = require('webpack').container; module.exports = { mode: 'development', entry: './src/index.js', output: { publicPath: 'http://localhost:3000/', }, devServer: { port: 3000, }, module: { rules: [ { test: /\.jsx?$/, loader: 'babel-loader', exclude: /node_modules/, options: { presets: ['@babel/preset-react'], }, }, ], }, plugins: [ new ModuleFederationPlugin({ name: 'main_shell', remotes: { product_catalog: 'product_catalog@http://localhost:3001/remoteEntry.js', // Define the remote application }, shared: { react: { singleton: true, requiredVersion: '^18.0.0' }, 'react-dom': { singleton: true, requiredVersion: '^18.0.0' }, }, }), new HtmlWebpackPlugin({ template: './public/index.html', }), ], };
The remotes
field in the host's configuration tells Webpack where to find product_catalog
's remoteEntry.js
file. The shared
configuration here mirrors that of the remote, ensuring react
and react-dom
are singleton instances.
When the main-shell
application loads, it will dynamically fetch product_catalog
's remoteEntry.js
(a small manifest file) which tells it how to get the ProductCard
component. This enables runtime integration of physically separate applications.
Application Scenarios
Module Federation excels in scenarios where you need to:
- Break down monolithic frontends into micro-frontends: Each micro-frontend can be developed, deployed, and scaled independently by different teams.
- Share common components or libraries: Avoid duplicating code by sharing UI component libraries, utility functions, or even entire business logic modules across multiple applications.
- Implement A/B testing or feature flagging: Dynamically load different versions of components or features based on user groups or experimental conditions without re-deploying the entire application.
- Build extensible platforms: Allow third-party developers or other internal teams to extend the core platform by providing their own modules that can be federated in.
Conclusion
Module Federation stands as a paradigm shift in how we approach large-scale frontend development. By empowering developers to compose multiple independent applications into a single, coherent experience, it significantly enhances scalability, simplifies deployments, and fosters code reuse across diverse teams and projects. This powerful Webpack feature truly enables a modular future for web applications, moving us closer to the promise of truly independent and robust micro-frontend architectures.