Today, let’s dive into something hardcore—using Webpack Module Federation for micro-frontend architecture in practice. Micro-frontends have gained traction in recent years, especially in large teams and projects where massive applications need to be split into smaller, independently developed and deployed modules that work seamlessly together. Webpack Module Federation is a game-changer, enabling module sharing and dynamic loading, making it a lifesaver for micro-frontend setups.
What Are Micro-Frontends and Module Federation?
Let’s start with a quick overview of micro-frontends. Imagine a massive frontend project with hundreds of thousands of lines of code, a team of dozens, and modules like product listings, orders, user profiles, and payments all crammed into one repository. Every small change requires rebuilding and redeploying the entire project, which is slow and error-prone. Micro-frontends solve this by breaking the app into smaller, independent applications, each developed and deployed separately but combined at runtime, like LEGO bricks.
Webpack Module Federation, introduced in Webpack 5, is designed for this scenario. It allows independently built Webpack projects to share modules at runtime, such as a React component, utility function, or state management logic, without duplicating code. Its core capabilities include:
- Dynamic Loading: Load modules from remote sources at runtime without bundling them together upfront.
- Module Sharing: Multiple apps can share libraries (e.g., React, Lodash) to avoid redundant loading.
- Independent Deployment: Each module can be built and deployed independently without interference.
Using Module Federation for micro-frontends offers flexibility. Teams can work at their own pace, mix tech stacks (e.g., React and Vue), and reduce build times and bundle sizes. Let’s jump into the code and build a simple micro-frontend architecture to see Module Federation in action.
Project Scenario and Structure
Imagine we’re building an e-commerce platform with three modules:
- Shell App: The main app, handling layout, navigation, and loading other modules.
- Product Module: Displays product listings.
- Cart Module: Manages the user’s selected items.
Each module is developed and deployed independently but integrates seamlessly at runtime. We’ll use Module Federation to let the Shell dynamically load the Product and Cart modules while sharing React and ReactDOM to avoid duplicate loading.
The project structure looks like this:
shell-app/
├── src/
│ ├── App.jsx
│ ├── bootstrap.jsx
│ ├── index.js
├── webpack.config.js
├── package.json
product-app/
├── src/
│ ├── ProductList.jsx
│ ├── index.js
├── webpack.config.js
├── package.json
cart-app/
├── src/
│ ├── Cart.jsx
│ ├── index.js
├── webpack.config.js
├── package.json
Each app is an independent React project, built with Webpack 5 and configured with Module Federation for module exposure and loading. Let’s start with the Shell app.
Configuring the Shell App
The Shell app is the “container” for the micro-frontend, responsible for loading the Product and Cart modules. Let’s set up its Webpack configuration and code.
Shell App Webpack Configuration
The Shell app needs Module Federation to load remote modules and share common dependencies.
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
publicPath: 'http://localhost:3000/',
},
devServer: {
port: 3000,
},
module: {
rules: [
{
test: /\.jsx?$/,
loader: 'babel-loader',
options: {
presets: ['@babel/preset-react'],
},
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
new ModuleFederationPlugin({
name: 'shell',
remotes: {
ProductApp: 'product@http://localhost:3001/remoteEntry.js',
CartApp: 'cart@http://localhost:3002/remoteEntry.js',
},
shared: {
react: {
singleton: true,
requiredVersion: '^18.2.0',
},
'react-dom': {
singleton: true,
requiredVersion: '^18.2.0',
},
},
}),
],
};
Key points in this configuration:
-
name: Names the Shell app
shell
. Even though it doesn’t expose modules, it needs a name. -
remotes: Defines remote modules.
ProductApp
andCartApp
point to the Product and Cart modules’ entry files (remoteEntry.js
). For example,product@http://localhost:3001/remoteEntry.js
means the Product module runs atlocalhost:3001
, and Webpack fetches it from there. -
shared: Shares React and ReactDOM, ensuring all modules use the same instance with
singleton: true
and a specificrequiredVersion
to prevent conflicts. - output.publicPath: Sets the public path for correct resource URLs.
-
devServer: Runs the Shell on port
3000
for development.
Shell App Code
Now, let’s write the Shell app’s code to dynamically load and render the Product and Cart modules.
// src/index.js
import('./bootstrap');
// src/bootstrap.jsx
import React, { Suspense, lazy } from 'react';
import { createRoot } from 'react-dom/client';
const ProductList = lazy(() => import('ProductApp/ProductList'));
const Cart = lazy(() => import('CartApp/Cart'));
function App() {
return (
<div>
<h1>E-Commerce Shell</h1>
<nav>
<a href="#products">Products</a>
<a href="#cart">Cart</a>
</nav>
<Suspense fallback={<div>Loading...</div>}>
<section>
<h2>Product List</h2>
<ProductList />
</section>
<section>
<h2>Shopping Cart</h2>
<Cart />
</section>
</Suspense>
</div>
);
}
const root = createRoot(document.getElementById('root'));
root.render(<App />);
<!-- public/index.html -->
<!DOCTYPE html>
<html>
<head>
<title>E-Commerce Shell</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
// package.json
{
"name": "shell-app",
"version": "1.0.0",
"scripts": {
"start": "webpack serve"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@babel/preset-react": "^7.20.0",
"babel-loader": "^9.1.0",
"html-webpack-plugin": "^5.5.0",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.0",
"webpack-dev-server": "^4.11.0"
}
}
Key points:
-
Dynamic Loading: Uses
React.lazy
andimport('ProductApp/ProductList')
to load remote modules dynamically.ProductApp/ProductList
corresponds to the exposed component in the Product module, matching theremotes
in Webpack. -
Suspense: Wraps dynamic components with
Suspense
to show aLoading...
placeholder until modules load. - bootstrap.jsx: Separates React rendering logic to handle Webpack Module Federation with React 18’s SSR requirements.
-
Simple Navigation: Adds a basic
<nav>
for switching between Product and Cart views.
Configuring the Product Module
The Product module displays a product list, runs independently on localhost:3001
, and exposes the ProductList
component to the Shell via Module Federation.
Product Module Webpack Configuration
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
publicPath: 'http://localhost:3001/',
},
devServer: {
port: 3001,
},
module: {
rules: [
{
test: /\.jsx?$/,
loader: 'babel-loader',
options: {
presets: ['@babel/preset-react'],
},
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
new ModuleFederationPlugin({
name: 'product',
filename: 'remoteEntry.js',
exposes: {
'./ProductList': './src/ProductList.jsx',
},
shared: {
react: {
singleton: true,
requiredVersion: '^18.2.0',
},
'react-dom': {
singleton: true,
requiredVersion: '^18.2.0',
},
},
}),
],
};
Key points:
-
name: Names the module
product
, matching theremotes
in the Shell’s config. -
filename: Exposes the entry file as
remoteEntry.js
, which the Shell uses to load the module. -
exposes: Exposes the
ProductList
component at./src/ProductList.jsx
, accessible externally asproduct/ProductList
. - shared: Shares React and ReactDOM, ensuring version consistency with the Shell.
Product Module Code
// src/ProductList.jsx
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function ProductList() {
const [products, setProducts] = useState([]);
useEffect(() => {
axios.get('https://api.example.com/products')
.then(res => setProducts(res.data))
.catch(err => console.error('Failed to fetch products:', err));
}, []);
return (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '20px' }}>
{products.map(product => (
<div key={product.id} style={{ width: '200px', border: '1px solid #ccc', padding: '10px' }}>
<img src={product.image} alt={product.name} style={{ width: '100%' }} />
<h3>{product.name}</h3>
<p>${product.price}</p>
<button onClick={() => alert('Add to cart clicked!')}>Add to Cart</button>
</div>
))}
</div>
);
}
export default ProductList;
// src/index.js
import('./bootstrap');
// src/bootstrap.jsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import ProductList from './ProductList';
const root = createRoot(document.getElementById('root'));
root.render(<ProductList />);
<!-- public/index.html -->
<!DOCTYPE html>
<html>
<head>
<title>Product App</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
// package.json
{
"name": "product-app",
"version": "1.0.0",
"scripts": {
"start": "webpack serve"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"axios": "^1.6.0"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@babel/preset-react": "^7.20.0",
"babel-loader": "^9.1.0",
"html-webpack-plugin": "^5.5.0",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.0",
"webpack-dev-server": "^4.11.0"
}
}
Key points:
- ProductList: A simple React component fetching product data via API and rendering a list.
- axios: Simulates fetching data; replace with a real API in production.
-
Standalone: The module can run independently at
localhost:3001
for debugging.
Configuring the Cart Module
The Cart module manages the shopping cart, runs on localhost:3002
, and exposes the Cart
component.
Cart Module Webpack Configuration
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
publicPath: 'http://localhost:3002/',
},
devServer: {
port: 3002,
},
module: {
rules: [
{
test: /\.jsx?$/,
loader: 'babel-loader',
options: {
presets: ['@babel/preset-react'],
},
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
new ModuleFederationPlugin({
name: 'cart',
filename: 'remoteEntry.js',
exposes: {
'./Cart': './src/Cart.jsx',
},
shared: {
react: {
singleton: true,
requiredVersion: '^18.2.0',
},
'react-dom': {
singleton: true,
requiredVersion: '^18.2.0',
},
},
}),
],
};
Similar to the Product module, it exposes the Cart
component and runs on port 3002
.
Cart Module Code
// src/Cart.jsx
import React, { useState } from 'react';
function Cart() {
const [items, setItems] = useState([
{ id: 1, name: 'Product 1', price: 10 },
{ id: 2, name: 'Product 2', price: 20 },
]);
const removeItem = (id) => {
setItems(items.filter(item => item.id !== id));
};
return (
<div>
<h3>Shopping Cart</h3>
{items.length === 0 ? (
<p>Your cart is empty</p>
) : (
<ul>
{items.map(item => (
<li key={item.id}>
{item.name} - ${item.price}
<button onClick={() => removeItem(item.id)}>Remove</button>
</li>
))}
</ul>
)}
</div>
);
}
export default Cart;
// src/index.js
import('./bootstrap');
// src/bootstrap.jsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import Cart from './Cart';
const root = createRoot(document.getElementById('root'));
root.render(<Cart />);
<!-- public/index.html -->
<!DOCTYPE html>
<html>
<head>
<title>Cart App</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
// package.json
{
"name": "cart-app",
"version": "1.0.0",
"scripts": {
"start": "webpack serve"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@babel/preset-react": "^7.20.0",
"babel-loader": "^9.1.0",
"html-webpack-plugin": "^5.5.0",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.0",
"webpack-dev-server": "^4.11.0"
}
}
The Cart module is simple, displaying a static list with remove functionality.
Running and Debugging
Run all three apps:
# In shell-app directory
npm install
npm start
# In product-app directory
npm install
npm start
# In cart-app directory
npm install
npm start
Visit http://localhost:3000
in your browser. The Shell app should load the Product and Cart modules, displaying the product list and cart. The Product module fetches data, and the Cart module shows a static list with interactive buttons.
If loading fails, check:
-
Ports: Ensure Shell (
3000
), Product (3001
), and Cart (3002
) use the correct ports. -
CORS: The
remoteEntry.js
files need CORS support. Webpack Dev Server handles this in development, but production requires server-side CORS configuration. -
Version Consistency: Verify React and ReactDOM versions match across all modules in
package.json
.
Cross-Module Communication
The modules load, but Product and Cart don’t interact yet. For example, clicking “Add to Cart” in Product should update the Cart module. Module Federation doesn’t handle state management, so we’ll use events, Context, or an external state library (e.g., Redux).
Using Events for Communication
A simple approach is custom events. Product triggers an “add to cart” event, and Cart listens to update its state.
Update ProductList:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function ProductList() {
const [products, setProducts] = useState([]);
useEffect(() => {
axios.get('https://api.example.com/products')
.then(res => setProducts(res.data))
.catch(err => console.error('Failed to fetch products:', err));
}, []);
const addToCart = (product) => {
const event = new CustomEvent('addToCart', { detail: product });
window.dispatchEvent(event);
};
return (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '20px' }}>
{products.map(product => (
<div key={product.id} style={{ width: '200px', border: '1px solid #ccc', padding: '10px' }}>
<img src={product.image} alt={product.name} style={{ width: '100%' }} />
<h3>{product.name}</h3>
<p>${product.price}</p>
<button onClick={() => addToCart(product)}>Add to Cart</button>
</div>
))}
</div>
);
}
export default ProductList;
Update Cart:
import React, { useState, useEffect } from 'react';
function Cart() {
const [items, setItems] = useState([]);
useEffect(() => {
const handleAddToCart = (event) => {
setItems(prev => [...prev, event.detail]);
};
window.addEventListener('addToCart', handleAddToCart);
return () => window.removeEventListener('addToCart', handleAddToCart);
}, []);
const removeItem = (id) => {
setItems(items.filter(item => item.id !== id));
};
return (
<div>
<h3>Shopping Cart</h3>
{items.length === 0 ? (
<p>Your cart is empty</p>
) : (
<ul>
{items.map(item => (
<li key={item.id}>
{item.name} - ${item.price}
<button onClick={() => removeItem(item.id)}>Remove</button>
</li>
))}
</ul>
)}
</div>
);
}
export default Cart;
Clicking “Add to Cart” in Product updates the Cart module’s list. Event-based communication is simple but less ideal for complex scenarios requiring persistent state or multi-module interactions.
Using Context for Communication
For a more robust solution, use React Context in the Shell to manage state, allowing Product and Cart to communicate via Context.
Update Shell’s App:
// src/App.jsx
import React, { Suspense, lazy, createContext, useState } from 'react';
const ProductList = lazy(() => import('ProductApp/ProductList'));
const Cart = lazy(() => import('CartApp/Cart'));
export const CartContext = createContext();
function App() {
const [cartItems, setCartItems] = useState([]);
const addToCart = (product) => {
setCartItems(prev => [...prev, product]);
};
const removeFromCart = (id) => {
setCartItems(prev => prev.filter(item => item.id !== id));
};
return (
<CartContext.Provider value={{ cartItems, addToCart, removeFromCart }}>
<div>
<h1>E-Commerce Shell</h1>
<nav>
<a href="#products">Products</a>
<a href="#cart">Cart</a>
</nav>
<Suspense fallback={<div>Loading...</div>}>
<section>
<h2>Product List</h2>
<ProductList />
</section>
<section>
<h2>Shopping Cart</h2>
<Cart />
</section>
</Suspense>
</div>
</CartContext.Provider>
);
}
export default App;
Update ProductList:
import React, { useState, useEffect, useContext } from 'react';
import axios from 'axios';
import { CartContext } from 'shell/App';
function ProductList() {
const [products, setProducts] = useState([]);
const { addToCart } = useContext(CartContext);
useEffect(() => {
axios.get('https://api.example.com/products')
.then(res => setProducts(res.data))
.catch(err => console.error('Failed to fetch products:', err));
}, []);
return (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '20px' }}>
{products.map(product => (
<div key={product.id} style={{ width: '200px', border: '1px solid #ccc', padding: '10px' }}>
<img src={product.image} alt={product.name} style={{ width: '100%' }} />
<h3>{product.name}</h3>
<p>${product.price}</p>
<button onClick={() => addToCart(product)}>Add to Cart</button>
</div>
))}
</div>
);
}
export default ProductList;
Update Cart:
import React, { useContext } from 'react';
import { CartContext } from 'shell/App';
function Cart() {
const { cartItems, removeFromCart } = useContext(CartContext);
return (
<div>
<h3>Shopping Cart</h3>
{cartItems.length === 0 ? (
<p>Your cart is empty</p>
) : (
<ul>
{cartItems.map(item => (
<li key={item.id}>
{item.name} - ${item.price}
<button onClick={() => removeFromCart(item.id)}>Remove</button>
</li>
))}
</ul>
)}
</div>
);
}
export default Cart;
Key points:
-
Context Sharing: The Shell defines
CartContext
withcartItems
,addToCart
, andremoveFromCart
. -
Cross-Module Context: Product and Cart access the Shell’s Context via
import('shell/App')
. Expose the Shell’sApp
module in Webpack:
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
publicPath: 'http://localhost:3000/',
},
devServer: {
port: 3000,
},
module: {
rules: [
{
test: /\.jsx?$/,
loader: 'babel-loader',
options: {
presets: ['@babel/preset-react'],
},
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
new ModuleFederationPlugin({
name: 'shell',
filename: 'remoteEntry.js',
remotes: {
ProductApp: 'product@http://localhost:3001/remoteEntry.js',
CartApp: 'cart@http://localhost:3002/remoteEntry.js',
},
exposes: {
'./App': './src/App.jsx',
},
shared: {
react: {
singleton: true,
requiredVersion: '^18.2.0',
},
'react-dom': {
singleton: true,
requiredVersion: '^18.2.0',
},
},
}),
],
};
Context centralizes state management, ideal for complex scenarios.
Module Style Isolation
Independent module development risks CSS conflicts, e.g., Product and Cart both defining button
styles. Use CSS Modules or Shadow DOM for isolation.
Using CSS Modules
Update ProductList with CSS Modules:
import React, { useState, useEffect, useContext } from 'react';
import axios from 'axios';
import { CartContext } from 'shell/App';
import styles from './ProductList.module.css';
function ProductList() {
const [products, setProducts] = useState([]);
const { addToCart } = useContext(CartContext);
useEffect(() => {
axios.get('https://api.example.com/products')
.then(res => setProducts(res.data))
.catch(err => console.error('Failed to fetch products:', err));
}, []);
return (
<div className={styles.container}>
{products.map(product => (
<div key={product.id} className={styles.product}>
<img src={product.image} alt={product.name} className={styles.image} />
<h3>{product.name}</h3>
<p>${product.price}</p>
<button onClick={() => addToCart(product)} className={styles.button}>Add to Cart</button>
</div>
))}
</div>
);
}
export default ProductList;
/* src/ProductList.module.css */
.container {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.product {
width: 200px;
border: 1px solid #ccc;
padding: 10px;
}
.image {
width: 100%;
}
.button {
background: #007bff;
color: white;
padding: 8px;
border: none;
border-radius: 3px;
}
Update Product’s Webpack config for CSS Modules:
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
publicPath: 'http://localhost:3001/',
},
devServer: {
port: 3001,
},
module: {
rules: [
{
test: /\.jsx?$/,
loader: 'babel-loader',
options: {
presets: ['@babel/preset-react'],
},
},
{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true,
},
},
],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
new ModuleFederationPlugin({
name: 'product',
filename: 'remoteEntry.js',
exposes: {
'./ProductList': './src/ProductList.jsx',
},
shared: {
react: {
singleton: true,
requiredVersion: '^18.2.0',
},
'react-dom': {
singleton: true,
requiredVersion: '^18.2.0',
},
},
}),
],
};
CSS Modules compile class names into unique hashes (e.g., ProductList_container_abc123
), preventing conflicts. Apply similar changes to the Cart module.
Production Deployment
For production, consider:
-
Static Asset Hosting: Deploy each module’s
remoteEntry.js
and assets to a CDN or static server, ensuringpublicPath
points to the correct URL. -
CORS Configuration: Remote module servers must support CORS to allow the Shell to fetch
remoteEntry.js
. -
Version Management: Use
requiredVersion
orversion
to enforce consistent shared module versions. - Dynamic Remote URLs: In production, remote URLs may vary. Set them dynamically in the Shell:
// src/bootstrap.jsx
import React, { Suspense, lazy } from 'react';
import { createRoot } from 'react-dom/client';
const ProductList = lazy(() => import('ProductApp/ProductList'));
const Cart = lazy(() => import('CartApp/Cart'));
if (process.env.NODE_ENV === 'production') {
__webpack_public_path__ = 'https://cdn.example.com/shell/';
window.ProductApp = 'product@https://cdn.example.com/product/remoteEntry.js';
window.CartApp = 'cart@https://cdn.example.com/cart/remoteEntry.js';
}
function App() {
return (
<div>
<h1>E-Commerce Shell</h1>
<nav>
<a href="#products">Products</a>
<a href="#cart">Cart</a>
</nav>
<Suspense fallback={<div>Loading...</div>}>
<section>
<h2>Product List</h2>
<ProductList />
</section>
<section>
<h2>Shopping Cart</h2>
<Cart />
</section>
</Suspense>
</div>
);
}
const root = createRoot(document.getElementById('root'));
root.render(<App />);
Error Handling and Fallbacks
Module loading may fail (e.g., network issues, remote module offline). Use React.Suspense
and an error boundary:
// src/ErrorBoundary.jsx
import React from 'react';
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return <div>Failed to load module. Please try again later.</div>;
}
return this.props.children;
}
}
export default ErrorBoundary;
Update App:
import React, { Suspense, lazy } from 'react';
import ErrorBoundary from './ErrorBoundary';
const ProductList = lazy(() => import('ProductApp/ProductList'));
const Cart = lazy(() => import('CartApp/Cart'));
function App() {
return (
<div>
<h1>E-Commerce Shell</h1>
<nav>
<a href="#products">Products</a>
<a href="#cart">Cart</a>
</nav>
<Suspense fallback={<div>Loading...</div>}>
<section>
<h2>Product List</h2>
<ErrorBoundary>
<ProductList />
</ErrorBoundary>
</section>
<section>
<h2>Shopping Cart</h2>
<ErrorBoundary>
<Cart />
</ErrorBoundary>
</section>
</Suspense>
</div>
);
}
export default App;
Failed module loads now show a user-friendly error instead of crashing the app.
Dynamic Module Loading
To reduce initial load time, load modules on demand, e.g., only load the Cart module when the user clicks the “Cart” link:
import React, { Suspense, lazy, useState } from 'react';
import ErrorBoundary from './ErrorBoundary';
const ProductList = lazy(() => import('ProductApp/ProductList'));
function App() {
const [showCart, setShowCart] = useState(false);
const loadCart = () => {
setShowCart(true);
};
return (
<div>
<h1>E-Commerce Shell</h1>
<nav>
<a href="#products">Products</a>
<a href="#cart" onClick={loadCart}>Cart</a>
</nav>
<Suspense fallback={<div>Loading...</div>}>
<section>
<h2>Product List</h2>
<ErrorBoundary>
<ProductList />
</ErrorBoundary>
</section>
{showCart && (
<section>
<h2>Shopping Cart</h2>
<ErrorBoundary>
<Suspense fallback={<div>Loading Cart...</div>}>
{lazy(() => import('CartApp/Cart'))()}
</Suspense>
</ErrorBoundary>
</section>
)}
</Suspense>
</div>
);
}
export default App;
The Cart module loads only when the “Cart” link is clicked, reducing initial load time.
Sharing Utility Modules
Beyond React and ReactDOM, modules can share utility functions or components. For example, Product and Cart can share a price formatting function from a utility module.
Create a utils-app
:
// utils-app/webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
publicPath: 'http://localhost:3003/',
},
devServer: {
port: 3003,
},
module: {
rules: [
{
test: /\.jsx?$/,
loader: 'babel-loader',
options: {
presets: ['@babel/preset-react'],
},
},
],
},
plugins: [
new ModuleFederationPlugin({
name: 'utils',
filename: 'remoteEntry.js',
exposes: {
'./formatPrice': './src/formatPrice.js',
},
shared: {
react: {
singleton: true,
requiredVersion: '^18.2.0',
},
'react-dom': {
singleton: true,
requiredVersion: '^18.2.0',
},
},
}),
],
};
// utils-app/src/formatPrice.js
export function formatPrice(price) {
return `$${price.toFixed(2)}`;
}
Add remotes
to Shell, Product, and Cart Webpack configs:
remotes: {
ProductApp: 'product@http://localhost:3001/remoteEntry.js',
CartApp: 'cart@http://localhost:3002/remoteEntry.js',
UtilsApp: 'utils@http://localhost:3003/remoteEntry.js',
}
Use in ProductList:
import React, { useState, useEffect, useContext } from 'react';
import axios from 'axios';
import { CartContext } from 'shell/App';
import { formatPrice } from 'UtilsApp/formatPrice';
import styles from './ProductList.module.css';
function ProductList() {
const [products, setProducts] = useState([]);
const { addToCart } = useContext(CartContext);
useEffect(() => {
axios.get('https://api.example.com/products')
.then(res => setProducts(res.data))
.catch(err => console.error('Failed to fetch products:', err));
}, []);
return (
<div className={styles.container}>
{products.map(product => (
<div key={product.id} className={styles.product}>
<img src={product.image} alt={product.name} className={styles.image} />
<h3>{product.name}</h3>
<p>{formatPrice(product.price)}</p>
<button onClick={() => addToCart(product)} className={styles.button}>Add to Cart</button>
</div>
))}
</div>
);
}
export default ProductList;
Now Product and Cart share formatPrice
, keeping the code DRY (Don’t Repeat Yourself).
Conclusion
This guide covers the complete process of building a micro-frontend with Webpack Module Federation. We configured the Shell, Product, and Cart modules for dynamic loading, module sharing, cross-module communication, style isolation, error handling, and utility sharing. Each module is developed and deployed independently, integrated seamlessly via Module Federation. React and ReactDOM are shared to avoid duplication, Context manages state, and CSS Modules prevent style conflicts. Hopefully, this code and analysis help you get started with Module Federation and build your own micro-frontend architecture!
Top comments (0)