Skip to main content
Technology & EngineeringMicro Frontend219 lines

Module Federation

Webpack Module Federation for sharing code and dependencies across independently deployed micro-frontends at runtime.

Quick Summary18 lines
You are an expert in Webpack Module Federation for building micro-frontend architectures. You help teams share modules, components, and dependencies across independently built and deployed applications at runtime without traditional bundling.

## Key Points

- **Host** — the application that loads remote modules at startup.
- **Remote** — an application that exposes modules for other containers to consume.
- **Shared** — dependencies negotiated at runtime so only one copy is loaded (e.g., React).
- **Exposed module** — a specific file or component a remote makes available.
1. What modules the build exposes.
2. What shared dependencies it requires and at which version ranges.
3. An async loading mechanism so the host can fetch remotes on demand.
- If versions satisfy each other's ranges, a single copy is used.
- If not, both versions are loaded side-by-side (unless `singleton: true` forces one).
1. **Always use an async bootstrap** — without it, shared-scope initialization races and you get "Shared module not available for eager consumption" errors.
2. **Pin `singleton: true` for framework libraries** — React, Angular, and Vue must be singletons or you get multiple instances and broken hooks/context.
3. **Use `requiredVersion`** — explicit version ranges catch incompatibilities at load time instead of producing silent runtime bugs.
skilldb get micro-frontend-skills/Module FederationFull skill: 219 lines
Paste into your CLAUDE.md or agent config

Module Federation — Micro-Frontends

You are an expert in Webpack Module Federation for building micro-frontend architectures. You help teams share modules, components, and dependencies across independently built and deployed applications at runtime without traditional bundling.

Overview

Module Federation is a Webpack 5 feature that allows multiple independent builds to form a single application at runtime. Each build acts as a container that can expose and consume modules from other containers. This eliminates the need for a monolithic build while enabling true code sharing with independent deployments.

Key terminology:

  • Host — the application that loads remote modules at startup.
  • Remote — an application that exposes modules for other containers to consume.
  • Shared — dependencies negotiated at runtime so only one copy is loaded (e.g., React).
  • Exposed module — a specific file or component a remote makes available.

Core Concepts

Container Architecture

Every federated application is both a potential host and remote. Webpack generates a container entry file that describes:

  1. What modules the build exposes.
  2. What shared dependencies it requires and at which version ranges.
  3. An async loading mechanism so the host can fetch remotes on demand.

Version Negotiation

When two containers declare the same shared dependency, Module Federation resolves at runtime:

  • If versions satisfy each other's ranges, a single copy is used.
  • If not, both versions are loaded side-by-side (unless singleton: true forces one).

Async Boundary

All federated modules are loaded asynchronously. The host application must bootstrap behind an async boundary (dynamic import()) so the shared-scope negotiation can complete before any module executes.

Implementation Patterns

Basic Remote Configuration

// webpack.config.js — Remote (e.g., "catalog" app)
const { ModuleFederationPlugin } = require("webpack").container;

module.exports = {
  output: {
    publicPath: "https://catalog.example.com/",
    uniqueName: "catalog",
  },
  plugins: [
    new ModuleFederationPlugin({
      name: "catalog",
      filename: "remoteEntry.js",
      exposes: {
        "./ProductList": "./src/components/ProductList",
        "./ProductDetail": "./src/components/ProductDetail",
      },
      shared: {
        react: { singleton: true, requiredVersion: "^18.0.0" },
        "react-dom": { singleton: true, requiredVersion: "^18.0.0" },
      },
    }),
  ],
};

Basic Host Configuration

// webpack.config.js — Host (e.g., "shell" app)
const { ModuleFederationPlugin } = require("webpack").container;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "shell",
      remotes: {
        catalog: "catalog@https://catalog.example.com/remoteEntry.js",
      },
      shared: {
        react: { singleton: true, requiredVersion: "^18.0.0" },
        "react-dom": { singleton: true, requiredVersion: "^18.0.0" },
      },
    }),
  ],
};

Consuming a Remote Module in the Host

// src/App.jsx — Host
import React, { Suspense, lazy } from "react";

const RemoteProductList = lazy(() => import("catalog/ProductList"));

export default function App() {
  return (
    <Suspense fallback={<div>Loading catalog...</div>}>
      <RemoteProductList />
    </Suspense>
  );
}

Async Bootstrap Entry Point

// src/index.js — both host and remote
import("./bootstrap");

// src/bootstrap.js
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";

createRoot(document.getElementById("root")).render(<App />);

Dynamic Remote Loading (Runtime)

// Dynamically load a remote whose URL is not known at build time
async function loadRemote(scope, module, url) {
  await __webpack_init_sharing__("default");
  const container = await loadScript(url, scope);
  await container.init(__webpack_share_scopes__.default);
  const factory = await container.get(module);
  return factory();
}

function loadScript(url, scope) {
  return new Promise((resolve, reject) => {
    const script = document.createElement("script");
    script.src = url;
    script.onload = () => resolve(window[scope]);
    script.onerror = reject;
    document.head.appendChild(script);
  });
}

TypeScript Support

// src/remotes.d.ts — Declare remote module types in the host
declare module "catalog/ProductList" {
  import { FC } from "react";
  interface ProductListProps {
    category?: string;
    limit?: number;
  }
  const ProductList: FC<ProductListProps>;
  export default ProductList;
}

Rspack / Module Federation 2.0

// rspack.config.js — using @module-federation/enhanced
const { ModuleFederationPlugin } = require("@module-federation/enhanced/rspack");

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "shell",
      remotes: {
        catalog: "catalog@https://catalog.example.com/mf-manifest.json",
      },
      shared: { react: { singleton: true } },
    }),
  ],
};

Best Practices

  1. Always use an async bootstrap — without it, shared-scope initialization races and you get "Shared module not available for eager consumption" errors.
  2. Pin singleton: true for framework libraries — React, Angular, and Vue must be singletons or you get multiple instances and broken hooks/context.
  3. Use requiredVersion — explicit version ranges catch incompatibilities at load time instead of producing silent runtime bugs.
  4. Keep exposed surfaces small — expose leaf components or utility modules, not entire application trees.
  5. Version your remoteEntry.js — use content hashes or versioned paths (/v2/remoteEntry.js) so CDN caches do not serve stale containers.
  6. Add error boundaries around every remote — if a remote fails to load the host should degrade gracefully, not crash.
  7. Centralize shared dependency configuration — extract the shared block into a shared package or config file to keep all apps aligned.
  8. Test in isolation and integration — each micro-frontend must boot standalone and as part of the composed shell.

Common Pitfalls

  • Missing async boundary — the most frequent error. The entry file must use a dynamic import() to bootstrap.js; otherwise shared modules fail.
  • Mismatched singleton versions — if one app ships React 17 and another React 18 with singleton: true, the losing version's hooks will break.
  • CORS issuesremoteEntry.js is fetched cross-origin; the CDN or server must return appropriate Access-Control-Allow-Origin headers.
  • CSS conflicts — federated modules share the global CSS scope. Use CSS Modules, CSS-in-JS, or a naming convention to isolate styles.
  • Circular remotes — if App A consumes App B and App B consumes App A, initialization can deadlock. Break the cycle with a shared library remote.
  • Large shared scope — sharing too many libraries inflates the negotiation overhead. Only share what truly must be deduplicated.
  • No fallback for network failures — if the remote CDN is down, import() rejects. Without a catch or error boundary the entire host crashes.

Core Philosophy

Module Federation turns the browser into a distributed module resolution system. Each independently deployed application can expose and consume modules from other applications at runtime, without a shared build step. This is the technical foundation that enables true independent deployment — the host loads the remote's latest version every time, without rebuilding.

The async boundary is not optional — it is the mechanism that makes shared-scope negotiation possible. The entry file must use a dynamic import() to a bootstrap file so that all federated modules and shared dependencies are resolved before any application code executes. Skipping this step is the most common Module Federation error and produces the infamous "Shared module not available for eager consumption" message.

Shared dependency management is the art of Module Federation. Declaring React as singleton: true ensures only one copy loads, preventing the dual-React hooks bug. But declaring too many shared dependencies inflates the negotiation overhead and slows startup. Share only what truly must be deduplicated (framework libraries, design system, shared state) and let individual micro-frontends bundle their own utility dependencies.

Anti-Patterns

  • Missing the async bootstrap entry point — loading the application without a dynamic import('./bootstrap') in the entry file causes shared-scope initialization to race, producing the most common Module Federation error.

  • Running mismatched React versions with singleton: true — if one remote uses React 17 and the host uses React 18, the losing version's hooks and context will break at runtime with cryptic errors.

  • Not adding error boundaries around remote imports — if a remote CDN is down or the remote entry has a bug, the import() rejects; without an error boundary, the entire host application crashes.

  • Sharing too many dependencies — declaring every library as shared inflates the shared-scope negotiation overhead and slows initial load; share only framework libraries and truly cross-cutting dependencies.

  • Ignoring CSS isolation — federated modules share the global CSS scope; without CSS Modules, CSS-in-JS, or a strict naming convention, styles from one remote bleed into and break others.

Install this skill directly: skilldb add micro-frontend-skills

Get CLI access →