As enterprise applications scale, the traditional Single Page Application (SPA) monolith becomes a severe bottleneck. A monolith forces all developers into a single repository, causing 20-minute build times, merge conflict nightmares, and fragile deployments where a bug in the 'Footer' takes down the 'Checkout' page. Micro-Frontends solve this by applying the backend microservices philosophy to the UI: breaking the frontend into independently deployable, loosely coupled applications that seamlessly stitch together at runtime.


Module 1: The Integration Strategies

How do you stitch two React apps together? Historically, teams used Iframes (terrible UX, zero shared state), or Npm Packages (requires the host app to rebuild and redeploy every time a child package updates). Today, the industry standard is Run-Time Integration via Module Federation.


Module 2: Introduction to Webpack Module Federation

Introduced in Webpack 5, Module Federation allows a JavaScript application to dynamically load code from another application at runtime, over the network. Crucially, it manages dependencies intelligently: if both apps use react@18, Webpack only downloads it once.

The Core Concepts

  • Host (Shell): The main application that boots up. It provides the layout, routing, and global providers.
  • Remote (Micro-App): A standalone application that exposes specific components or functions.
  • Shared Dependencies: Libraries that should only be loaded once (like React, Zustand) to prevent bloated bundle sizes and state bugs.

Module 3: Configuring the Remote Application

Let's imagine 'Team Checkout' is building a Cart Widget. They configure their Webpack to expose this component as a remote.

cart-app/webpack.config.jsjavascript
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const deps = require('./package.json').dependencies;

module.exports = {
  // Standard Webpack config (entry, module rules for Babel/TS, etc)
  output: {
    publicPath: 'http://localhost:3002/', // The URL where this app is hosted
  },
  plugins: [
    new ModuleFederationPlugin({
      // The global variable name for this remote
      name: 'cartApp', 
      // The manifest file that tells the Host how to load the exposes
      filename: 'remoteEntry.js', 
      exposes: {
        // Expose the CartWidget component to the outside world
        './CartWidget': './src/components/CartWidget.jsx',
      },
      // Explicitly define shared dependencies
      shared: {
        ...deps,
        react: { singleton: true, requiredVersion: deps.react },
        'react-dom': { singleton: true, requiredVersion: deps['react-dom'] },
      },
    }),
  ],
};

When built, cart-app generates remoteEntry.js. This tiny file contains the map of where to find the chunks for CartWidget.


Module 4: Configuring the Host Application

The Host app (managed by the Core Platform Team) consumes the Remote.

host-app/webpack.config.jsjavascript
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const deps = require('./package.json').dependencies;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'hostApp',
      remotes: {
        // Map the internal namespace 'cartApp' to the remote's URL
        // In production, this URL would be injected via environment variables
        cartApp: 'cartApp@http://localhost:3002/remoteEntry.js',
      },
      shared: {
        ...deps,
        react: { singleton: true, requiredVersion: deps.react },
        'react-dom': { singleton: true, requiredVersion: deps['react-dom'] },
      },
    }),
  ],
};

Module 5: Consuming the Remote Component in React

Because the code is loaded over the network at runtime, we MUST use React's lazy and Suspense.

host-app/src/App.jsxjavascript
import React, { Suspense } from 'react';
import ErrorBoundary from './components/ErrorBoundary';

// Dynamically import from the namespace defined in Webpack
const RemoteCartWidget = React.lazy(() => import('cartApp/CartWidget'));

function App() {
  return (
    <div className="app-layout">
      <header>
        <h1>E-Commerce Micro-Frontend Host</h1>
        
        {/* Error boundaries prevent the Host from crashing if the Remote is offline */}
        <ErrorBoundary fallback={<p>Cart service currently unavailable.</p>}>
          <Suspense fallback={<div className="spinner">Loading Cart...</div>}>
            <RemoteCartWidget />
          </Suspense>
        </ErrorBoundary>
      </header>
      
      <main>
        <p>Main product list goes here...</p>
      </main>
    </div>
  );
}

export default App;

Module 6: Cross-App Communication & State Management

The golden rule of Micro-Frontends is: Do not tightly couple state. Avoid creating a massive global Redux store that every team depends on. Instead, communicate via generic web standards.

Communication Methods

  • Custom Events (Pub/Sub): The cleanest method. The Host fires an event, the Remote listens. It completely decouples the apps.
  • URL Routing: Passing state via query parameters (e.g., ?productId=123).
  • Shared Module: Exposing a tiny state management file (using Zustand) from the Host that remotes import.
host-app/src/ProductCard.jsxjavascript
// Host firing an event
function handleAddToCart(product) {
  const event = new CustomEvent('MICRO_ADD_TO_CART', { detail: product });
  window.dispatchEvent(event);
}
cart-app/src/CartWidget.jsxjavascript
// Remote listening for the event
import React, { useEffect, useState } from 'react';

export default function CartWidget() {
  const [items, setItems] = useState(0);

  useEffect(() => {
    const handler = (e) => setItems(prev => prev + 1);
    window.addEventListener('MICRO_ADD_TO_CART', handler);
    return () => window.removeEventListener('MICRO_ADD_TO_CART', handler);
  }, []);

  return <div>🛒 {items} Items</div>;
}

Module 7: CI/CD and Independent Deployment

The true power of this architecture is deployment. Team Checkout can make a change to CartWidget, push to their repo, and their CI/CD pipeline builds and deploys to their S3 bucket (cart-app.com). The moment it deploys, the Host app instantly serves the new Cart Widget to users without the Host ever needing to rebuild.