DEV Community

ThankGod Chibugwum Obobo
ThankGod Chibugwum Obobo

Posted on • Originally published at actocodes.hashnode.dev

Micro-Frontends Explained: How to Implement Module Federation

Frontend applications are growing in complexity. As teams scale and codebases expand, a single monolithic frontend becomes just as problematic as a monolithic backend, slow builds, deployment bottlenecks, and team conflicts over shared code.

Micro-frontends apply the same decomposition principles as microservices to the UI layer. And with Webpack Module Federation, implementing micro-frontends has never been more practical.

In this guide, you'll learn what micro-frontends are, when to use them, and how to set up Module Federation step-by-step, with real configuration examples you can apply to your own projects.

What Are Micro-Frontends?

A micro-frontend architecture breaks a web application into smaller, independently deployable UI units each owned by a separate team, built with its own toolchain, and composed together at runtime.

Think of it this way: instead of one giant React app that every team commits to, you have:

  • A shell app (host) that defines the layout and navigation.
  • Multiple remote apps (micro-frontends) that own specific features or pages.

Each remote can be developed, tested, and deployed independently without touching the host or other remotes.

When Should You Use Micro-Frontends?

Micro-frontends are best suited for:

  • Large teams working on the same product with minimal coordination overhead.
  • Multi-team organizations where different squads own distinct product areas.
  • Legacy modernization — incrementally replacing parts of an old app without a full rewrite.
  • Independent deployment cycles — when one team shouldn't be blocked by another's release schedule.

They're overkill for small teams or early-stage products. Start with a well-structured monorepo first.

What Is Webpack Module Federation?

Introduced in Webpack 5, Module Federation is a native plugin that allows JavaScript bundles to dynamically load code from other independently deployed applications at runtime.

Before Module Federation, sharing code across apps required publishing npm packages, a slow feedback loop. Module Federation eliminates that by letting apps expose and consume modules directly over the network.

Key concepts:
| Term | Description |
|------|-------------|
| Host | The shell app that consumes remote modules |
| Remote | An app that exposes modules to be consumed |
| Shared | Dependencies shared across host and remotes to avoid duplication |
| Exposes | The modules a remote makes available |

Step 1 — Project Structure

For this guide, we'll build two applications:

  • shell — the host application
  • product-app — a remote micro-frontend exposing a ProductList component
/micro-frontend-demo
  /shell           ← Host app
  /product-app     ← Remote app
Enter fullscreen mode Exit fullscreen mode

Each app is a standalone Webpack + React project. Initialize them separately:

mkdir shell product-app
cd shell && npm init -y && npm install webpack webpack-cli webpack-dev-server react react-dom
cd ../product-app && npm init -y && npm install webpack webpack-cli webpack-dev-server react react-dom
Enter fullscreen mode Exit fullscreen mode

Step 2 — Configure the Remote App (product-app)

The remote app exposes components for the host to consume. Configure ModuleFederationPlugin in its webpack.config.js:

// product-app/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  mode: 'development',
  devServer: { port: 3001 },
  plugins: [
    new ModuleFederationPlugin({
      name: 'productApp',
      filename: 'remoteEntry.js',
      exposes: {
        './ProductList': './src/components/ProductList',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
      },
    }),
  ],
};
Enter fullscreen mode Exit fullscreen mode

Key properties explained:

  • name — unique identifier for this remote.
  • filename — the manifest file the host will fetch (remoteEntry.js).
  • exposes — maps public aliases to local module paths.
  • shared — prevents React from being loaded twice (critical for hooks to work correctly).

Step 3 — Configure the Host App (shell)

The shell app consumes the remote by referencing its remoteEntry.js URL:

// shell/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  mode: 'development',
  devServer: { port: 3000 },
  plugins: [
    new ModuleFederationPlugin({
      name: 'shell',
      remotes: {
        productApp: 'productApp@http://localhost:3001/remoteEntry.js',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
      },
    }),
  ],
};
Enter fullscreen mode Exit fullscreen mode

The remote reference follows the pattern: name@url/remoteEntry.js

Step 4 — Consume the Remote Component

In your shell app, import the remote component using a dynamic import with React.lazy. This is required because Module Federation loads code asynchronously at runtime:

// shell/src/App.tsx
import React, { Suspense, lazy } from 'react';

const ProductList = lazy(() => import('productApp/ProductList'));

export default function App() {
  return (
    <div>
      <h1>My Store</h1>
      <Suspense fallback={<div>Loading products...</div>}>
        <ProductList />
      </Suspense>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

When the shell renders <ProductList />, Webpack fetches the component's bundle from the product-app server at runtime — not at build time.

Step 5 — Sharing State Across Micro-Frontends

One of the trickiest aspects of micro-frontends is cross-app state management. A few proven approaches:

URL as State

Use the URL (query params, path segments) as the source of truth for shared state. It's universally accessible and framework-agnostic.

Custom Events (Web APIs)

Use the browser's native CustomEvent API for lightweight communication between micro-frontends:

// In product-app — dispatch an event
window.dispatchEvent(new CustomEvent('product:selected', { detail: { id: 42 } }));

// In shell — listen for the event
window.addEventListener('product:selected', (e) => {
  console.log('Selected product:', e.detail.id);
});
Enter fullscreen mode Exit fullscreen mode

Shared State Library

If deeper integration is needed, expose a shared store (e.g., Zustand or a Redux slice) via Module Federation's shared config and consume it across remotes.

Step 6 — Deployment Considerations

In production, each micro-frontend is deployed independently to its own origin (CDN bucket, cloud function, or container). Update your remote URLs to use environment variables:

remotes: {
  productApp: `productApp@${process.env.PRODUCT_APP_URL}/remoteEntry.js`,
},
Enter fullscreen mode Exit fullscreen mode

Deployment best practices:

  • Version your remote entries — use content-hashed filenames or versioned paths (/v2/remoteEntry.js) to prevent stale cache issues.
  • Health checks — monitor remoteEntry.js availability; a failed remote shouldn't crash the entire shell.
  • Fallback UI — always wrap remote imports in <Suspense> and error boundaries.
  • CI/CD independence — each remote should have its own pipeline with no dependency on the host's release cycle.

Common Pitfalls to Avoid

Duplicate dependencies: Forgetting to mark react and react-dom as singleton in shared config causes multiple React instances breaking hooks and context. Always enforce singletons for peer dependencies.

Tight coupling through contracts: If remotes depend on props or APIs defined by the host, you've recreated the monolith problem. Remotes should be self-contained and communicate via events or shared state patterns.

Over-decomposing the UI: Not every component needs to be a micro-frontend. Decompose at the page or feature level, not the component level.

Conclusion

Webpack Module Federation makes micro-frontends practical for production teams. By independently developing, deploying, and composing UI modules, you unlock true frontend scalability, shorter release cycles, clearer ownership, and the freedom to evolve each part of your UI without coordinating a full-app deployment.

Start with a single remote extracted from your monolith, validate the workflow end-to-end, and expand incrementally. The architecture rewards patience and deliberate boundaries.

Building micro-frontends with a different bundler like Vite or Rspack? Let me know in the comments, a follow-up guide might be on the way.

Top comments (0)