modern-react-spa

Chapter 26

Module Federation in Depth

Module Federation for React 19.2 — host/remote/shared scope, Vite + Rspack setups, runtime vs build-time, sharing React/router/store, and the six failure modes you will hit.

Published 2026-05-23

🎯 Chapter Goal — After this chapter you can explain host / remote / shared scope without hand-waving, set up a 1-host + 2-remote architecture on Vite using @module-federation/vite, choose between runtime and build-time federation with clear reasons, and diagnose the six most common federation failures by reading the browser console alone.

🧭 Prerequisites — Ch 17 (Vite), Ch 19 (monorepo), Ch 25 (multi-team architecture).

Canonical reference: https://module-federation.io/ — the dedicated home of the post-Webpack-5 Module Federation project. Every API in this chapter must match what’s documented there. If terminology drifts, the site wins.


🔹 26.1 The mental model

Most MF outages come from a wrong mental model, not a config typo.

Three concepts

  • Host — the app the user loads first; orchestrates everything.
  • Remote — a separately-deployed app that exposes one or more modules.
  • Shared scope — a runtime map of { packageName → version → loaded module } that the host and remotes use to deduplicate React, the router, etc.
                      ┌──────────────────────────────┐
                      │  Host SPA   (acme-shell)     │
                      │                              │
                      │   ┌──────────────────────┐   │
                      │   │ shared scope         │   │
                      │   │  react@19.2          │◀──┼── one copy
                      │   │  react-dom@19.2      │   │
                      │   │  zustand@5.0         │   │
                      │   └──────────────────────┘   │
                      └───────▲──────────────▲───────┘
                              │              │
            remote: invoices.acme   remote: tenants.acme
            exposes ./InvoiceApp    exposes ./TenantApp

🔹 26.2 Federation on Vite and Rspack

Uses the packages from https://module-federation.io/. We are not using the legacy Webpack-5-only ModuleFederationPlugin; we are using the project’s modern, bundler-agnostic stack.

Vite — @module-federation/vite

📄 host config

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { federation } from '@module-federation/vite';

export default defineConfig({
  plugins: [
    react(),
    federation({
      name: 'acme-shell',
      remotes: {
        invoices: 'http://localhost:5174/remoteEntry.js',
        tenants:  'http://localhost:5175/remoteEntry.js',
      },
      shared: {
        react:       { singleton: true, requiredVersion: '^19.2.0' },
        'react-dom': { singleton: true, requiredVersion: '^19.2.0' },
      },
    }),
  ],
});

📄 remote config (acme-invoices)

export default defineConfig({
  plugins: [
    react(),
    federation({
      name: 'invoices',
      filename: 'remoteEntry.js',
      exposes: {
        './InvoiceApp': './src/InvoiceApp.tsx',
      },
      shared: {
        react:       { singleton: true, requiredVersion: '^19.2.0' },
        'react-dom': { singleton: true, requiredVersion: '^19.2.0' },
      },
    }),
  ],
  build: {
    target: 'esnext',
    modulePreload: false,
  },
});

📄 consuming the remote from the host

import { lazy, Suspense } from 'react';
const InvoiceApp = lazy(() => import('invoices/InvoiceApp'));  // ← MF magic specifier

<Suspense fallback={<Spinner />}>
  <InvoiceApp />
</Suspense>

Rspack / Webpack — @module-federation/enhanced

The recommended replacement for Webpack 5’s built-in ModuleFederationPlugin. Familiar to Webpack-MF users; adds manifest support, runtime plugins, type sharing.

Pick Rspack when you’re already on Webpack 5 and have non-trivial federation config. Pick Vite when starting fresh.

Cross-bundler runtime — @module-federation/runtime

A bundler-agnostic runtime loader. Used in runtime federation (next section).


🔹 26.3 Runtime vs build-time federation

Build-time → remote URLs known when host builds. Simple, but a new remote requires a host rebuild.

Runtime → remotes resolved at runtime from a Module Federation Manifest (the JSON descriptor format from module-federation.io → “Manifest”). Lets you deploy a new remote independently.

📄 runtime federation — host wiring

import { init, loadRemote } from '@module-federation/runtime';

// Fetch the manifest at boot:
const manifest = await fetch('/federation/manifest.json').then((r) => r.json());

init({
  name: 'acme-shell',
  remotes: manifest.remotes,   // [{ name: 'invoices', entry: '...' }, ...]
});

// Later, load a remote module:
const { default: InvoiceApp } = await loadRemote<{ default: React.ComponentType }>('invoices/InvoiceApp');

Trade-off: one extra HTTP round-trip at boot (the manifest fetch) vs deployment independence (new remotes added without redeploying the host).

For a 2-team setup: build-time is simpler. For a 10-team setup: runtime is worth the round-trip.


🔹 26.4 Sharing React, the router, the store

Always-singletons

  • react, react-dom — version mismatch → invariant errors.
  • Your router, whichever you picked in Ch 5 — react-router (v7 — the book’s default), @tanstack/react-router, or v6 if you’re on legacy. Federating with two different routers across host and remote almost never works; pick one and singleton-share it. Context identity must match, or you get “useNavigate must be used inside Router” errors.

Often-singletons

  • The global store (Zustand / Jotai / Redux) — so state is shared across the federation.
  • The design system — to avoid duplicated CSS-in-JS instances.
shared: {
  react:       { singleton: true, requiredVersion: '^19.2.0' },
  'react-dom': { singleton: true, requiredVersion: '^19.2.0' },
  'react-router': { singleton: true, requiredVersion: '^7.0.0' },  // ← Ch 5 §5.1 choice
  // OR (if you picked TanStack Router in Ch 5 §5.2):
  // '@tanstack/react-router': { singleton: true, requiredVersion: '^1.0.0' },
  zustand:     { singleton: true },
  '@acme/ui':  { singleton: true },   // ← the design system
},

⚠️ A non-singleton React in any remote → “Invalid hook call.” The single most common federation bug.

🔹 26.5 Failure modes — the six bugs you’ll hit

1. Singleton mismatch — “Invalid hook call”

Symptom: the remote renders, hooks throw immediately. Console: Invalid hook call. Hooks can only be called inside the body of a function component.

Cause: two Reacts loaded. The remote bundled its own.

Fix: singleton: true on react in both host and every remote.

2. Eager consumption

Symptom: “Shared module is not available for eager consumption” at boot.

Cause: the host imports a remote synchronously. The remote tries to access shared deps before the host has registered them.

Fix: always import remotes via React.lazy(() => import('remote/X')) or import('remote/X').then(...).

3. CORS on remoteEntry.js

Symptom: works locally, breaks in staging. Console: CORS error fetching remoteEntry.js.

Cause: the remote’s hosting doesn’t set Access-Control-Allow-Origin for the host’s domain.

Fix: configure CORS on the remote’s static-host (Cloudflare, S3, etc.). Allow the host’s exact origin, not *.

4. Public path drift

Symptom: remote loads in dev; in production, remote’s assets 404 from http://localhost:5174/... instead of the prod URL.

Cause: publicPath not set per environment.

Fix: set publicPath: 'auto' (recent MF versions handle it) or per-env explicit URLs.

5. Type-safety loss

Symptom: import InvoiceApp from 'invoices/InvoiceApp' has type any.

Cause: TypeScript doesn’t know about the federated module.

Fix: module-federation.io’s type-sharing feature (the host fetches type descriptors from each remote at install time). Or hand-declare module shapes:

declare module 'invoices/InvoiceApp' {
  const App: React.ComponentType;
  export default App;
}

6. Style leakage

Symptom: global CSS from a remote bleeds into the host’s design, or vice versa.

Cause: remote ships a CSS reset or global selectors.

Fix: scope all CSS in the remote (CSS modules, scoped utilities, or a wrapper class). Audit :root, body, * selectors in remote source.


🔹 26.6 Versioning shared dependencies safely

The peril: host runs React 19.2.0; remote bumps to React 19.3.0 and assumes it. Their requiredVersion: '^19.2.0' allows it, but the host’s pinned 19.2.0 is what loads. The remote crashes on a 19.3-only API.

The discipline:

  • All federation participants pin shared deps to the same range.
  • Bumps to shared deps are coordinated across host + all remotes.
  • Use strictVersion: true (newer MF feature) to fail loudly on mismatch instead of letting the host’s version win.

Rolling a React minor:

  1. Bump in a non-critical remote first. Deploy. Observe.
  2. Bump in the host. Deploy. Observe.
  3. Bump in every other remote. Deploy.

Step 1 alone catches most breakages (a remote that needed 19.3 will fail; revert; investigate).


🪤 Common Pitfalls

  1. Forgetting singleton: true on react → “Invalid hook call” in the remote.
  2. Importing a remote synchronously instead of via React.lazy.
  3. Hardcoding remote URLs across environments.
  4. CSS resets from remote leaking into host’s design system.
  5. Type exports forgotten — host gets any for remote components.
  6. Forgetting to bump shared-deps manifest when React minor-bumps.
  7. Federating something that should be a package — if the team can publish it instead, do that.

✅ Recap

  • Host, remote, shared scope — internalize before touching config.
  • Vite and Rspack both have first-class MF stories in 2026.
  • Runtime federation buys deployment independence at the cost of a manifest fetch.
  • Singleton mismatches cause the loudest bugs; CSS leaks cause the quietest ones.
  • Treat MF as a contract, not just a build config.

🔗 Further Reading

  • https://module-federation.io/ — start at “Guide” → “Getting Started”, then “Concept” → “How It Works”.
  • module-federation.io → “Framework / Vite” / “Plugin / Enhanced” / “Runtime” / “Manifest” / “DevTools”.
  • “Practical Module Federation” (Jack Herrington) — Webpack-era foundations.
  • Ch 27 — running different React versions across federations.

In the book — not on the site

Each topic has an 🧠 Under-the-hood subsection — the algorithm, the data structures, what React DevTools surfaces, debugging recipes. Plus a 🧪 hands-on lab per chapter with a starter repo. Reserved for the book.

Topics in this chapter (6)
  1. 26.1 The mental model
  2. 26.2 Federation on Vite and Rspack
  3. 26.3 Runtime vs build-time federation
  4. 26.4 Sharing React, the router, the store
  5. 26.5 Failure modes — the six bugs you’ll hit
  6. 26.6 Versioning shared dependencies safely