Introduction to Enterprise Microfrontend Suite

Welcome to Chapter 16! In this chapter, we’re diving deep into the world of microfrontends by building a practical Enterprise Microfrontend Suite. As organizations scale, their frontend applications often become monolithic giants, difficult to manage, deploy, and scale across multiple teams. Microfrontends offer a powerful solution, bringing the benefits of microservices to the user interface layer.

You’ll learn how to architect a large-scale React application by breaking it down into smaller, independently deployable units. We’ll leverage Webpack Module Federation, the industry standard for achieving runtime composition of microfrontends, to create a host application that dynamically loads and orchestrates multiple remote applications. This approach significantly enhances team autonomy, improves deployment speed, and allows for technology diversity within a single user experience.

To get the most out of this chapter, you should have a solid understanding of React fundamentals, basic Webpack configuration, and the importance of modularity in software design. We’ll build upon concepts of component-based architecture and state management, applying them in a distributed frontend environment. Get ready to embark on a journey that shapes how modern enterprise UIs are built!

Core Concepts: Enterprise Microfrontend Architecture

Building an enterprise-grade application often means dealing with multiple teams, varied feature sets, and a long product lifecycle. This is where microfrontends shine.

What are Microfrontends?

Imagine a complex web application, like a large e-commerce platform or an internal dashboard for a big company. Traditionally, this would be built as a single, monolithic frontend application. A microfrontend architecture breaks this monolith into smaller, self-contained applications that can be developed, deployed, and managed independently by different teams. Think of it as applying the “microservices” philosophy to the frontend.

Each microfrontend typically owns a distinct business domain or feature set (e.g., “User Management,” “Product Catalog,” “Order History”). These smaller applications then compose a single, cohesive user experience, usually orchestrated by a “host” or “shell” application.

Why Microfrontends for Enterprise?

For large organizations, microfrontends offer compelling advantages:

  1. Team Autonomy: Different teams can work on different microfrontends without stepping on each other’s toes, leading to faster development cycles and fewer coordination headaches.
  2. Independent Deployments: Each microfrontend can be deployed independently. A bug fix in the “User Management” app doesn’t require redeploying the entire “Product Catalog” or “Dashboard Shell.”
  3. Technology Diversity: Teams can choose the best technology stack for their specific microfrontend. While we’ll focus on React, one microfrontend could theoretically use Vue, another Angular, and still coexist (though this adds complexity).
  4. Scalability: Easier to scale development efforts. Onboarding new teams or contractors is simpler when they can own a specific, smaller part of the codebase.
  5. Resilience: An issue in one microfrontend might not bring down the entire application, depending on how isolation is implemented.

A Real-World Failure Story

Consider a large financial institution that built a single-page application (SPA) for its internal trading desk. Over several years, the codebase grew to millions of lines of JavaScript, managed by dozens of teams. Every deployment became a high-risk event, requiring extensive cross-team coordination and testing. A critical bug in a seemingly unrelated module could halt the entire deployment pipeline for days. This monolithic approach led to slow feature delivery, high developer frustration, and significant operational overhead. Adopting a microfrontend strategy could have allowed teams to iterate and deploy independently, drastically reducing risk and increasing agility.

Webpack Module Federation: The Enabler

While the concept of microfrontends existed before, Webpack Module Federation (WMF), introduced in Webpack 5, revolutionized their practical implementation. WMF allows multiple separate builds to form a single application. This means a Webpack build can expose modules (components, utilities, pages) to other Webpack builds, and consume modules from other builds at runtime.

What it is:

WMF is a plugin for Webpack that enables JavaScript applications to dynamically load code from other applications at runtime. It’s not just about sharing libraries; it’s about sharing entire modules or components.

Why it’s important:

Before WMF, achieving true runtime composition of independently deployed frontends was complex, often involving custom build scripts, shared externals, or iframe-based solutions. WMF provides a standardized, efficient, and robust way to do this directly within the Webpack build process.

How it functions:

WMF defines two main roles for applications:

  • Host (or Container): An application that consumes modules exposed by other applications. It’s the “shell” that orchestrates the user experience.
  • Remote (or Microfrontend): An application that exposes its modules to be consumed by other applications. Each remote is an independent microfrontend.

When a host needs a module from a remote, WMF handles the dynamic loading of the remote’s JavaScript bundle. It also intelligently manages shared dependencies (like React itself) to avoid downloading multiple copies, ensuring optimal performance.

Architectural Mental Model

Let’s visualize how a host application interacts with multiple remote microfrontends.

graph LR User[User's Browser] --> HostApp[Host Application] subgraph Remote Microfrontends Remote1[User Management App] Remote2[Product Catalog App] Remote3[Reporting App] end HostApp -->|Loads dynamically| Remote1 HostApp -->|Loads dynamically| Remote2 HostApp -->|Loads dynamically| Remote3 style HostApp fill:#f9f,stroke:#333,stroke-width:2px style Remote1 fill:#bbf,stroke:#333,stroke-width:2px style Remote2 fill:#bbf,stroke:#333,stroke-width:2px style Remote3 fill:#bbf,stroke:#333,stroke-width:2px

Mental Model: The User’s Browser interacts with the Host Application (Dashboard Shell), which then dynamically loads and integrates various Remote Microfrontends (User Management, Product Catalog, Reporting) as needed.

Shared Dependencies and Versioning

One of WMF’s most powerful features is its ability to handle shared dependencies. When configuring Module Federation, you specify libraries (like react, react-dom, react-router-dom) that should be shared. WMF then ensures that these dependencies are only loaded once, and that all microfrontends use the same, compatible version specified by the host, if available. This is crucial for avoiding bundle bloat and runtime conflicts.

Routing Strategy

In a microfrontend setup, routing is a critical consideration. A common pattern is to have the host application manage the primary routing. When a user navigates to a specific path (e.g., /users), the host application dynamically loads the UserManagementApp microfrontend and renders it. The remote microfrontend itself might have its own internal routing for sub-features (e.g., /users/profile, /users/settings).

This centralized routing approach simplifies navigation and ensures a consistent URL structure across the entire suite.

State Management Across Microfrontends

Sharing state between microfrontends can be tricky. Here are common approaches:

  1. URL Parameters / Local Storage: Simple data passing for less critical information.
  2. Shared Utility Library: A common library containing a global state management solution (e.g., Zustand, Jotai, or a custom event bus) that all microfrontends can access. This needs careful versioning.
  3. Custom Event Bus: A publish-subscribe pattern where microfrontends can emit and listen for custom events.
  4. Backend for Frontend (BFF): For very complex interactions, a BFF layer can orchestrate state.

For our project, we’ll explore a basic shared utility library approach to demonstrate cross-microfrontend communication.

Step-by-Step Implementation: Building an Enterprise Microfrontend Suite

Let’s get our hands dirty and build a simple enterprise microfrontend suite using React and Webpack Module Federation. We’ll set up a monorepo containing a host application and two remote applications.

Current Versions (as of 2026-02-14):

  • React: ^19.0.0 (assuming React 19 is the stable release)
  • Webpack: ^5.90.0
  • Node.js: ^20.0.0 (LTS)

1. Project Setup: Monorepo with pnpm

We’ll use pnpm workspaces for efficient monorepo management.

First, create a new directory for our project:

mkdir enterprise-mf-suite
cd enterprise-mf-suite

Initialize pnpm and create a pnpm-workspace.yaml file:

pnpm init
# Then create pnpm-workspace.yaml
touch pnpm-workspace.yaml

Edit pnpm-workspace.yaml to define our workspaces:

# pnpm-workspace.yaml
packages:
  - 'packages/*'

Now, create the packages directory:

mkdir packages

2. Host Application: dashboard-shell

This will be our main application, responsible for loading and displaying the remote microfrontends.

mkdir packages/dashboard-shell
cd packages/dashboard-shell
pnpm init

Install React and Webpack dependencies:

pnpm add react@^19.0.0 react-dom@^19.0.0 react-router-dom@^6.22.0
pnpm add -D webpack@^5.90.0 webpack-cli@^5.0.0 webpack-dev-server@^4.0.0 html-webpack-plugin@^5.0.0 babel-loader@^9.0.0 @babel/core@^7.23.0 @babel/preset-react@^7.23.0 @babel/preset-env@^7.23.0 css-loader@^6.8.0 style-loader@^3.3.3

Create src/index.js, src/App.js, and public/index.html:

// packages/dashboard-shell/src/index.js
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';

const container = document.getElementById('app');
const root = createRoot(container);
root.render(<App />);
// packages/dashboard-shell/src/App.js
import React, { Suspense } from 'react';
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';

// We'll dynamically import these later
// const UserManagementApp = React.lazy(() => import('user_management/UserManagementPage'));
// const ProductCatalogApp = React.lazy(() => import('product_catalog/ProductCatalogPage'));

const Home = () => <h2>Welcome to the Enterprise Dashboard!</h2>;
const Loading = () => <p>Loading Microfrontend...</p>;

function App() {
  return (
    <Router>
      <div style={{ fontFamily: 'Arial, sans-serif', padding: '20px' }}>
        <h1>Enterprise Dashboard Shell</h1>
        <nav>
          <ul style={{ listStyle: 'none', padding: 0 }}>
            <li style={{ display: 'inline-block', marginRight: '15px' }}>
              <Link to="/">Home</Link>
            </li>
            <li style={{ display: 'inline-block', marginRight: '15px' }}>
              {/* <Link to="/users">User Management</Link> */}
            </li>
            <li style={{ display: 'inline-block' }}>
              {/* <Link to="/products">Product Catalog</Link> */}
            </li>
          </ul>
        </nav>
        <hr />
        <Suspense fallback={<Loading />}>
          <Routes>
            <Route path="/" element={<Home />} />
            {/* <Route path="/users" element={<UserManagementApp />} /> */}
            {/* <Route path="/products" element={<ProductCatalogApp />} /> */}
          </Routes>
        </Suspense>
      </div>
    </Router>
  );
}

export default App;
<!-- packages/dashboard-shell/public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Dashboard Shell</title>
</head>
<body>
    <div id="app"></div>
</body>
</html>

Now, the crucial part: webpack.config.js for Module Federation.

// packages/dashboard-shell/webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');

module.exports = {
  entry: './src/index.js',
  mode: 'development',
  devServer: {
    port: 3000,
    historyApiFallback: true, // Important for React Router
  },
  output: {
    publicPath: 'http://localhost:3000/', // Host's public path
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-react', '@babel/preset-env'],
          },
        },
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'dashboard_shell',
      remotes: {
        // We will define our remote microfrontends here
        // user_management: 'user_management@http://localhost:3001/remoteEntry.js',
        // product_catalog: 'product_catalog@http://localhost:3002/remoteEntry.js',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^19.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^19.0.0' },
        'react-router-dom': { singleton: true, requiredVersion: '^6.22.0' },
      },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

Explanation:

  • entry: The main entry point for our React app.
  • devServer.port: The port where our host application will run (3000).
  • output.publicPath: Crucial for Module Federation. This tells Webpack where to find assets from this bundle.
  • ModuleFederationPlugin:
    • name: A unique name for this application (the host).
    • remotes: An object where we’ll list the remote applications we want to consume. The format is remote_name: 'remote_name@remote_url/remoteEntry.js'.
    • shared: This defines dependencies that should be shared across microfrontends. singleton: true ensures only one instance of the module is loaded, and requiredVersion helps prevent version conflicts.

Add scripts to package.json:

// packages/dashboard-shell/package.json
{
  "name": "dashboard-shell",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "webpack serve",
    "build": "webpack --mode production"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "react-router-dom": "^6.22.0"
  },
  "devDependencies": {
    "@babel/core": "^7.23.0",
    "@babel/preset-env": "^7.23.0",
    "@babel/preset-react": "^7.23.0",
    "babel-loader": "^9.0.0",
    "css-loader": "^6.8.0",
    "html-webpack-plugin": "^5.0.0",
    "style-loader": "^3.3.3",
    "webpack": "^5.90.0",
    "webpack-cli": "^5.0.0",
    "webpack-dev-server": "^4.0.0"
  }
}

3. Remote Application 1: user-management-app

This microfrontend will provide a “User Management” page.

cd ../../
mkdir packages/user-management-app
cd packages/user-management-app
pnpm init

Install React and Webpack dependencies (similar to host):

pnpm add react@^19.0.0 react-dom@^19.0.0
pnpm add -D webpack@^5.90.0 webpack-cli@^5.0.0 webpack-dev-server@^4.0.0 html-webpack-plugin@^5.0.0 babel-loader@^9.0.0 @babel/core@^7.23.0 @babel/preset-react@^7.23.0 @babel/preset-env@^7.23.0 css-loader@^6.8.0 style-loader@^3.3.3

Create src/index.js, src/UserManagementPage.js, and public/index.html:

// packages/user-management-app/src/index.js
import React from 'react';
import { createRoot } from 'react-dom/client';
import UserManagementPage from './UserManagementPage';

// This is for running the remote in isolation for development
const container = document.getElementById('app');
if (container) {
  const root = createRoot(container);
  root.render(<UserManagementPage />);
}
// packages/user-management-app/src/UserManagementPage.js
import React from 'react';

const UserManagementPage = () => {
  const users = [
    { id: 1, name: 'Alice Smith', email: 'alice@example.com' },
    { id: 2, name: 'Bob Johnson', email: 'bob@example.com' },
    { id: 3, name: 'Charlie Brown', email: 'charlie@example.com' },
  ];

  return (
    <div style={{ border: '1px solid #ccc', padding: '15px', borderRadius: '5px', backgroundColor: '#e0f7fa' }}>
      <h3>User Management Microfrontend</h3>
      <h4>Current Users:</h4>
      <ul>
        {users.map(user => (
          <li key={user.id}>
            {user.name} ({user.email})
          </li>
        ))}
      </ul>
      <button onClick={() => alert('Add User functionality coming soon!')}>Add New User</button>
    </div>
  );
};

export default UserManagementPage;
<!-- packages/user-management-app/public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>User Management App</title>
</head>
<body>
    <div id="app"></div>
</body>
</html>

Now, webpack.config.js for this remote:

// packages/user-management-app/webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');

module.exports = {
  entry: './src/index.js',
  mode: 'development',
  devServer: {
    port: 3001, // Different port from host
    historyApiFallback: true,
  },
  output: {
    publicPath: 'http://localhost:3001/', // Remote's public path
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-react', '@babel/preset-env'],
          },
        },
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'user_management', // Unique name for this remote
      filename: 'remoteEntry.js', // The file that contains the exposed modules
      exposes: {
        './UserManagementPage': './src/UserManagementPage.js', // Expose our component
      },
      shared: {
        react: { singleton: true, requiredVersion: '^19.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^19.0.0' },
      },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

Explanation:

  • devServer.port: This remote runs on port 3001.
  • output.publicPath: This remote’s public path.
  • ModuleFederationPlugin:
    • name: A unique name for this remote application.
    • filename: The name of the bundle file that contains the exposed modules and WMF runtime. This is the file the host will fetch.
    • exposes: An object where we define what modules this remote will make available to others. The key is the “internal” name the host will use, and the value is the path to the module within this remote.
    • shared: Similar to the host, we share react and react-dom to avoid duplication. Note that react-router-dom is not shared here because this remote doesn’t use it directly; it relies on the host’s router.

Add scripts to package.json:

// packages/user-management-app/package.json
{
  "name": "user-management-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "webpack serve",
    "build": "webpack --mode production"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  },
  "devDependencies": {
    "@babel/core": "^7.23.0",
    "@babel/preset-env": "^7.23.0",
    "@babel/preset-react": "^7.23.0",
    "babel-loader": "^9.0.0",
    "css-loader": "^6.8.0",
    "html-webpack-plugin": "^5.0.0",
    "style-loader": "^3.3.3",
    "webpack": "^5.90.0",
    "webpack-cli": "^5.0.0",
    "webpack-dev-server": "^4.0.0"
  }
}

4. Remote Application 2: product-catalog-app

This microfrontend will provide a “Product Catalog” page.

cd ../../
mkdir packages/product-catalog-app
cd packages/product-catalog-app
pnpm init

Install React and Webpack dependencies:

pnpm add react@^19.0.0 react-dom@^19.0.0
pnpm add -D webpack@^5.90.0 webpack-cli@^5.0.0 webpack-dev-server@^4.0.0 html-webpack-plugin@^5.0.0 babel-loader@^9.0.0 @babel/core@^7.23.0 @babel/preset-react@^7.23.0 @babel/preset-env@^7.23.0 css-loader@^6.8.0 style-loader@^3.3.3

Create src/index.js, src/ProductCatalogPage.js, and public/index.html:

// packages/product-catalog-app/src/index.js
import React from 'react';
import { createRoot } from 'react-dom/client';
import ProductCatalogPage from './ProductCatalogPage';

const container = document.getElementById('app');
if (container) {
  const root = createRoot(container);
  root.render(<ProductCatalogPage />);
}
// packages/product-catalog-app/src/ProductCatalogPage.js
import React from 'react';

const ProductCatalogPage = () => {
  const products = [
    { id: 101, name: 'Laptop Pro', price: 1200 },
    { id: 102, name: 'Wireless Mouse', price: 25 },
    { id: 103, name: 'Mechanical Keyboard', price: 90 },
  ];

  return (
    <div style={{ border: '1px solid #ccc', padding: '15px', borderRadius: '5px', backgroundColor: '#e8f5e9' }}>
      <h3>Product Catalog Microfrontend</h3>
      <h4>Available Products:</h4>
      <ul>
        {products.map(product => (
          <li key={product.id}>
            {product.name} - ${product.price}
          </li>
        ))}
      </ul>
      <button onClick={() => alert('View Product Details functionality coming soon!')}>View Details</button>
    </div>
  );
};

export default ProductCatalogPage;
<!-- packages/product-catalog-app/public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Product Catalog App</title>
</head>
<body>
    <div id="app"></div>
</body>
</html>

And its webpack.config.js:

// packages/product-catalog-app/webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');

module.exports = {
  entry: './src/index.js',
  mode: 'development',
  devServer: {
    port: 3002, // Different port from host and other remote
    historyApiFallback: true,
  },
  output: {
    publicPath: 'http://localhost:3002/',
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-react', '@babel/preset-env'],
          },
        },
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'product_catalog',
      filename: 'remoteEntry.js',
      exposes: {
        './ProductCatalogPage': './src/ProductCatalogPage.js',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^19.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^19.0.0' },
      },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

Add scripts to package.json:

// packages/product-catalog-app/package.json
{
  "name": "product-catalog-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "webpack serve",
    "build": "webpack --mode production"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  },
  "devDependencies": {
    "@babel/core": "^7.23.0",
    "@babel/preset-env": "^7.23.0",
    "@babel/preset-react": "^7.23.0",
    "babel-loader": "^9.0.0",
    "css-loader": "^6.8.0",
    "html-webpack-plugin": "^5.0.0",
    "style-loader": "^3.3.3",
    "webpack": "^5.90.0",
    "webpack-cli": "^5.0.0",
    "webpack-dev-server": "^4.0.0"
  }
}

5. Integrating Remotes into the Host

Now that our remotes are defined, let’s update the dashboard-shell to consume them.

First, update packages/dashboard-shell/webpack.config.js to include the remotes configuration:

// packages/dashboard-shell/webpack.config.js - update remotes section
// ...
      remotes: {
        user_management: 'user_management@http://localhost:3001/remoteEntry.js',
        product_catalog: 'product_catalog@http://localhost:3002/remoteEntry.js',
      },
// ...

Next, update packages/dashboard-shell/src/App.js to dynamically import and use the remote components:

// packages/dashboard-shell/src/App.js - update imports and Routes
import React, { Suspense } from 'react';
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';

// Dynamically import remote components using React.lazy
const UserManagementApp = React.lazy(() => import('user_management/UserManagementPage'));
const ProductCatalogApp = React.lazy(() => import('product_catalog/ProductCatalogPage'));

const Home = () => <h2>Welcome to the Enterprise Dashboard!</h2>;
const Loading = () => <p>Loading Microfrontend...</p>;

function App() {
  return (
    <Router>
      <div style={{ fontFamily: 'Arial, sans-serif', padding: '20px' }}>
        <h1>Enterprise Dashboard Shell</h1>
        <nav>
          <ul style={{ listStyle: 'none', padding: 0 }}>
            <li style={{ display: 'inline-block', marginRight: '15px' }}>
              <Link to="/">Home</Link>
            </li>
            <li style={{ display: 'inline-block', marginRight: '15px' }}>
              <Link to="/users">User Management</Link>
            </li>
            <li style={{ display: 'inline-block' }}>
              <Link to="/products">Product Catalog</Link>
            </li>
          </ul>
        </nav>
        <hr />
        <Suspense fallback={<Loading />}>
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/users" element={<UserManagementApp />} />
            <Route path="/products" element={<ProductCatalogApp />} />
          </Routes>
        </Suspense>
      </div>
    </Router>
  );
}

export default App;

Explanation:

  • We now use React.lazy to create a dynamically loading component. The import('user_management/UserManagementPage') syntax tells Webpack (via Module Federation) to fetch the UserManagementPage module from the user_management remote.
  • Suspense is a React feature that allows you to “wait” for some code to load and display a fallback while waiting. This is perfect for dynamically loading microfrontends.
  • New Link components and Route definitions are added to navigate to our microfrontends.

How it works at runtime:

sequenceDiagram participant Browser participant DashboardShell as Host (port 3000) participant UserMgmtApp as Remote (port 3001) participant ProductCatApp as Remote (port 3002) Browser->>DashboardShell: Request / (Load Host) DashboardShell-->>Browser: Host HTML & JS Browser->>DashboardShell: User clicks "User Management" link (/users) DashboardShell->>UserMgmtApp: Request remoteEntry.js for 'user_management' UserMgmtApp-->>DashboardShell: remoteEntry.js (contains UserManagementPage) DashboardShell->>Browser: Render UserManagementPage Browser->>DashboardShell: User clicks "Product Catalog" link (/products) DashboardShell->>ProductCatApp: Request remoteEntry.js for 'product_catalog' ProductCatApp-->>DashboardShell: remoteEntry.js (contains ProductCatalogPage) DashboardShell->>Browser: Render ProductCatalogPage

Sequence Diagram: The Browser loads the Host. When a user navigates to a microfrontend route, the Host dynamically requests the remoteEntry.js file from the respective Remote application, which then sends the necessary JavaScript. The Host then renders the microfrontend component within its own UI.

6. Running the Suite

To see this in action, you need to start all three applications simultaneously.

In three separate terminal windows, navigate to each package and run pnpm start:

Terminal 1:

cd packages/dashboard-shell
pnpm start

(This will start the host on http://localhost:3000)

Terminal 2:

cd packages/user-management-app
pnpm start

(This will start the user management remote on http://localhost:3001)

Terminal 3:

cd packages/product-catalog-app
pnpm start

(This will start the product catalog remote on http://localhost:3002)

Now, open your browser and navigate to http://localhost:3000. You should see the “Enterprise Dashboard Shell”. Click on “User Management” and “Product Catalog” links. You’ll observe the respective microfrontends loading and rendering within the host application.

7. Basic Shared State/Communication Example

Let’s demonstrate a very simple way for microfrontends to share information. We’ll create a shared utility library that the host and remotes can both consume.

First, create a new package for shared utilities:

cd ../../
mkdir packages/shared-utils
cd packages/shared-utils
pnpm init

Create src/eventBus.js:

// packages/shared-utils/src/eventBus.js
const subscribers = {};

export const subscribe = (eventType, callback) => {
  if (!subscribers[eventType]) {
    subscribers[eventType] = [];
  }
  subscribers[eventType].push(callback);
  return () => {
    subscribers[eventType] = subscribers[eventType].filter(cb => cb !== callback);
  };
};

export const publish = (eventType, data) => {
  if (subscribers[eventType]) {
    subscribers[eventType].forEach(callback => callback(data));
  }
};

// Example usage:
// subscribe('userAdded', (user) => console.log('User added:', user));
// publish('userAdded', { id: 4, name: 'David Lee' });

Explanation: This is a basic publish-subscribe pattern. Any part of the application (host or remote) can subscribe to an event type or publish an event with data.

Now, we need to make shared-utils available to our microfrontends via Module Federation. This means it needs its own webpack.config.js and package.json scripts, or we can expose it directly from the host. For simplicity and to show a common pattern, let’s make it a shared dependency in the host and remotes. For more complex shared utilities, a separate module-federated shared-library remote would be ideal.

For now, let’s just make sure it’s accessible. In a real-world scenario, you’d likely expose this eventBus from a dedicated shared-library microfrontend.

For this example, we’ll modify the UserManagementPage to publish an event when a user is added, and the DashboardShell to listen to it.

Update packages/user-management-app/src/UserManagementPage.js:

// packages/user-management-app/src/UserManagementPage.js
import React from 'react';
import { publish } from 'shared_utils/eventBus'; // We'll make this available through MF

const UserManagementPage = () => {
  const users = [
    { id: 1, name: 'Alice Smith', email: 'alice@example.com' },
    { id: 2, name: 'Bob Johnson', email: 'bob@example.com' },
    { id: 3, name: 'Charlie Brown', email: 'charlie@example.com' },
  ];

  const handleAddUser = () => {
    const newUser = { id: 4, name: 'New User', email: 'new@example.com' };
    alert(`Adding new user: ${newUser.name}. Publishing 'userAdded' event.`);
    publish('userAdded', newUser); // Publish an event
  };

  return (
    <div style={{ border: '1px solid #ccc', padding: '15px', borderRadius: '5px', backgroundColor: '#e0f7fa' }}>
      <h3>User Management Microfrontend</h3>
      <h4>Current Users:</h4>
      <ul>
        {users.map(user => (
          <li key={user.id}>
            {user.name} ({user.email})
          </li>
        ))}
      </ul>
      <button onClick={handleAddUser}>Add New User (and publish event)</button>
    </div>
  );
};

export default UserManagementPage;

Update packages/dashboard-shell/src/App.js:

// packages/dashboard-shell/src/App.js - add event bus logic
import React, { Suspense, useEffect } from 'react'; // Import useEffect
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
import { subscribe } from 'shared_utils/eventBus'; // We'll make this available through MF

const UserManagementApp = React.lazy(() => import('user_management/UserManagementPage'));
const ProductCatalogApp = React.lazy(() => import('product_catalog/ProductCatalogPage'));

const Home = () => <h2>Welcome to the Enterprise Dashboard!</h2>;
const Loading = () => <p>Loading Microfrontend...</p>;

function App() {
  useEffect(() => {
    // Subscribe to the 'userAdded' event from any microfrontend
    const unsubscribe = subscribe('userAdded', (user) => {
      console.log('Host received userAdded event:', user);
      alert(`Host: A new user '${user.name}' was added from a microfrontend! Check console.`);
    });

    return () => unsubscribe(); // Clean up subscription
  }, []);

  return (
    <Router>
      <div style={{ fontFamily: 'Arial, sans-serif', padding: '20px' }}>
        <h1>Enterprise Dashboard Shell</h1>
        <nav>
          <ul style={{ listStyle: 'none', padding: 0 }}>
            <li style={{ display: 'inline-block', marginRight: '15px' }}>
              <Link to="/">Home</Link>
            </li>
            <li style={{ display: 'inline-block', marginRight: '15px' }}>
              <Link to="/users">User Management</Link>
            </li>
            <li style={{ display: 'inline-block' }}>
              <Link to="/products">Product Catalog</Link>
            </li>
          </ul>
        </nav>
        <hr />
        <Suspense fallback={<Loading />}>
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/users" element={<UserManagementApp />} />
            <Route path="/products" element={<ProductCatalogApp />} />
          </Routes>
        </Suspense>
      </div>
    </Router>
  );
}

export default App;

Now, we need to configure shared-utils as its own remote, or expose it from one of the existing remotes. The cleanest way is to make shared-utils its own lightweight remote.

Create packages/shared-utils/webpack.config.js:

// packages/shared-utils/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');

module.exports = {
  entry: './src/eventBus.js', // Entry point for this shared module
  mode: 'development',
  devServer: {
    port: 3003, // A new port for our shared utilities remote
  },
  output: {
    publicPath: 'http://localhost:3003/',
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'shared_utils',
      filename: 'remoteEntry.js',
      exposes: {
        './eventBus': './src/eventBus.js',
      },
      // No shared React/ReactDOM needed if it only exposes plain JS utilities
    }),
  ],
};

Add scripts to packages/shared-utils/package.json:

// packages/shared-utils/package.json
{
  "name": "shared-utils",
  "version": "1.0.0",
  "description": "",
  "main": "src/eventBus.js",
  "scripts": {
    "start": "webpack serve",
    "build": "webpack --mode production"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^5.90.0",
    "webpack-cli": "^5.0.0",
    "webpack-dev-server": "^4.0.0"
  }
}

Finally, update packages/dashboard-shell/webpack.config.js and packages/user-management-app/webpack.config.js to consume shared_utils:

packages/dashboard-shell/webpack.config.js (add to remotes):

// packages/dashboard-shell/webpack.config.js - update remotes
// ...
      remotes: {
        user_management: 'user_management@http://localhost:3001/remoteEntry.js',
        product_catalog: 'product_catalog@http://localhost:3002/remoteEntry.js',
        shared_utils: 'shared_utils@http://localhost:3003/remoteEntry.js', // NEW
      },
// ...

packages/user-management-app/webpack.config.js (add to remotes):

// packages/user-management-app/webpack.config.js - update remotes
// ...
      remotes: {
        shared_utils: 'shared_utils@http://localhost:3003/remoteEntry.js', // NEW
      },
// ...

Now, you’ll need to run a fourth terminal for shared-utils:

Terminal 4:

cd packages/shared-utils
pnpm start

(This will start the shared utilities remote on http://localhost:3003)

With all four running, navigate to http://localhost:3000, go to “User Management”, and click “Add New User”. You should see an alert from the UserManagementApp and then another alert from the DashboardShell, demonstrating successful cross-microfrontend communication via the shared event bus!

Mini-Challenge: Adding a Reporting Microfrontend

Now it’s your turn!

Challenge: Create a new microfrontend called reporting-app. This app should expose a ReportingPage component that displays some dummy reports (e.g., “Sales Report,” “Traffic Report”). Integrate this new reporting-app into the dashboard-shell just like we did with the other two.

Steps to follow:

  1. Create a new directory packages/reporting-app.
  2. Initialize pnpm, install dependencies (React, Webpack, Babel).
  3. Create src/index.js, src/ReportingPage.js (with a simple “Reporting Microfrontend” heading and a list of dummy reports), and public/index.html.
  4. Configure webpack.config.js for reporting-app:
    • Give it a unique name (e.g., reporting_app).
    • Expose ./ReportingPage.
    • Assign it a new, unused port (e.g., 3004).
    • Share react and react-dom.
  5. Update packages/dashboard-shell/webpack.config.js to add reporting_app to its remotes.
  6. Update packages/dashboard-shell/src/App.js:
    • Add a React.lazy import for reporting_app/ReportingPage.
    • Add a new Link in the navigation for “/reports”.
    • Add a new Route for “/reports” to render the ReportingPage.
  7. Start all four applications (dashboard-shell, user-management-app, product-catalog-app, shared-utils) and your new reporting-app.
  8. Verify that you can navigate to the “/reports” path in the host, and your ReportingPage microfrontend renders correctly.

Hint: You can largely copy and adapt the structure from product-catalog-app for your new reporting-app, just remember to change names and ports!

What to observe/learn: Notice how easy it is to add a completely new, independently developed feature (a microfrontend) to your existing suite without modifying the core logic of the other microfrontends. This highlights the power of team autonomy and independent deployments.

Common Pitfalls & Troubleshooting

Working with microfrontends, especially with Module Federation, can introduce new challenges. Here’s how to tackle some common issues:

  1. Dependency Mismatch / Multiple React Instances:

    • Symptom: “Hooks can only be called inside the body of a function component” error, or unexpected UI behavior, especially when interacting with shared components.
    • Cause: Different microfrontends are loading their own versions of React (or other shared libraries) instead of using the single shared instance. This often happens if singleton: true is missing or requiredVersion is too permissive/restrictive.
    • Fix: Double-check your shared configuration in ModuleFederationPlugin for all applications (host and remotes). Ensure singleton: true is set for critical libraries like react, react-dom, and react-router-dom. Make sure requiredVersion matches across all apps or is compatible. If you’re bundling a shared library (like our eventBus), ensure it’s properly exposed and consumed.
  2. Remote Not Loading / Network Errors:

    • Symptom: Failed to fetch remoteEntry.js or Uncaught (in promise) Error: Cannot find module '...' in the browser console.
    • Cause: The remote application isn’t running, or the publicPath in its Webpack config is incorrect, or the URL in the host’s remotes config is wrong.
    • Fix:
      • Ensure all microfrontends (host and remotes) are running on their specified ports.
      • Verify that the publicPath in each webpack.config.js points to the correct http://localhost:PORT/.
      • Check the remotes entry in the host’s webpack.config.js to ensure the URL for remoteEntry.js is accurate (e.g., http://localhost:3001/remoteEntry.js).
      • Make sure filename: 'remoteEntry.js' is correctly set in the remote’s ModuleFederationPlugin.
  3. Styling Conflicts:

    • Symptom: Styles from one microfrontend bleed into another, or global styles are unexpectedly overridden.
    • Cause: CSS is global by default. Microfrontends might use conflicting class names or global selectors.
    • Fix:
      • Use CSS Modules (.module.css) for local scoping.
      • Adopt a CSS-in-JS library (e.g., Styled Components, Emotion) that provides scoped styles.
      • Utilize a shared design system or component library that provides consistent, encapsulated styling.
      • Consider Shadow DOM for stronger style isolation, though this adds complexity.
  4. Build Performance & Bundle Size:

    • Symptom: Slow build times, large initial bundle sizes for the host.
    • Cause: Inefficient Webpack configuration, too many shared dependencies, or not leveraging production optimizations.
    • Fix:
      • Ensure mode: 'production' for production builds.
      • Optimize shared dependencies: only share what’s truly needed, and ensure singleton: true is used appropriately.
      • Use code splitting effectively (which React.lazy and Suspense help with).
      • Implement lazy loading for routes and components.
      • Consider advanced Webpack optimizations and bundle analysis tools.

Summary

Congratulations! You’ve successfully built an Enterprise Microfrontend Suite using React and Webpack Module Federation. This is a significant step towards understanding modern, scalable frontend architectures.

Here are the key takeaways from this chapter:

  • Microfrontends break monolithic frontends into smaller, independently deployable applications, enhancing team autonomy and development speed.
  • Webpack Module Federation (WMF) is the core technology enabling runtime composition of these independent applications.
  • A Host application orchestrates the overall user experience, dynamically loading Remote microfrontends.
  • Shared dependencies (like React) are crucial for performance and consistency, and WMF handles them effectively.
  • Centralized routing in the host simplifies navigation across microfrontends.
  • Cross-microfrontend communication can be achieved through patterns like shared event buses or global state libraries.
  • Debugging microfrontends requires careful attention to dependency versions, publicPath configurations, and module exposure/consumption.

This project demonstrates how system design choices directly impact scalability, reliability, and developer productivity in modern React applications. As you continue your journey, remember these principles to build robust and maintainable large-scale systems.

What’s Next?

In the upcoming chapters, we’ll delve into more advanced topics such as performance optimization, observability, CI/CD strategies for microfrontends, and how to build a robust shared design system to ensure a consistent user experience across your suite. Keep building, keep learning!


This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.

References

  1. Webpack Module Federation Documentation
  2. React Official Documentation
  3. React Router DOM Documentation
  4. pnpm Workspaces Documentation
  5. MDN Web Docs: Learn React