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:
- Bump in a non-critical remote first. Deploy. Observe.
- Bump in the host. Deploy. Observe.
- Bump in every other remote. Deploy.
Step 1 alone catches most breakages (a remote that needed 19.3 will fail; revert; investigate).
🪤 Common Pitfalls
- Forgetting
singleton: trueonreact→ “Invalid hook call” in the remote. - Importing a remote synchronously instead of via
React.lazy. - Hardcoding remote URLs across environments.
- CSS resets from remote leaking into host’s design system.
- Type exports forgotten — host gets
anyfor remote components. - Forgetting to bump shared-deps manifest when React minor-bumps.
- 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.