DEV Community

Cover image for Webpack Module Federation in Action Micro-Frontend Architecture
Tianya School
Tianya School

Posted on

Webpack Module Federation in Action Micro-Frontend Architecture

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
Enter fullscreen mode Exit fullscreen mode

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',
        },
      },
    }),
  ],
};
Enter fullscreen mode Exit fullscreen mode

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 and CartApp point to the Product and Cart modules’ entry files (remoteEntry.js). For example, product@http://localhost:3001/remoteEntry.js means the Product module runs at localhost:3001, and Webpack fetches it from there.
  • shared: Shares React and ReactDOM, ensuring all modules use the same instance with singleton: true and a specific requiredVersion 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 />);
Enter fullscreen mode Exit fullscreen mode
<!-- public/index.html -->
<!DOCTYPE html>
<html>
<head>
  <title>E-Commerce Shell</title>
</head>
<body>
  <div id="root"></div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode
// 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"
  }
}
Enter fullscreen mode Exit fullscreen mode

Key points:

  • Dynamic Loading: Uses React.lazy and import('ProductApp/ProductList') to load remote modules dynamically. ProductApp/ProductList corresponds to the exposed component in the Product module, matching the remotes in Webpack.
  • Suspense: Wraps dynamic components with Suspense to show a Loading... 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',
        },
      },
    }),
  ],
};
Enter fullscreen mode Exit fullscreen mode

Key points:

  • name: Names the module product, matching the remotes 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 as product/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;
Enter fullscreen mode Exit fullscreen mode
// 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 />);
Enter fullscreen mode Exit fullscreen mode
<!-- public/index.html -->
<!DOCTYPE html>
<html>
<head>
  <title>Product App</title>
</head>
<body>
  <div id="root"></div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode
// 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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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',
        },
      },
    }),
  ],
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
// 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 />);
Enter fullscreen mode Exit fullscreen mode
<!-- public/index.html -->
<!DOCTYPE html>
<html>
<head>
  <title>Cart App</title>
</head>
<body>
  <div id="root"></div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode
// 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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Key points:

  • Context Sharing: The Shell defines CartContext with cartItems, addToCart, and removeFromCart.
  • Cross-Module Context: Product and Cart access the Shell’s Context via import('shell/App'). Expose the Shell’s App 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',
        },
      },
    }),
  ],
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
/* 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;
}
Enter fullscreen mode Exit fullscreen mode

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',
        },
      },
    }),
  ],
};
Enter fullscreen mode Exit fullscreen mode

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, ensuring publicPath 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 or version 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 />);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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',
        },
      },
    }),
  ],
};
Enter fullscreen mode Exit fullscreen mode
// utils-app/src/formatPrice.js
export function formatPrice(price) {
  return `$${price.toFixed(2)}`;
}
Enter fullscreen mode Exit fullscreen mode

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',
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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)