React 95 ⚛️ Micro-Frontend Architecture with Module Federation
Micro-frontends solve an organizational problem, not a technical one. When multiple teams contribute to the same React application, the codebase becomes a coordination bottleneck: a small change in one feature can break an unrelated feature, builds slow down as the bundle grows, and releases require synchronized deployment across every team . Module Federation addresses this by letting a host application load components from independently built and deployed remote applications at runtime, sharing dependencies like React as singletons so that each remote does not ship its own copy . The architecture is a runtime composition mechanism, not a build-time monorepo pattern—though the two are often combined.
The mechanics are straightforward in principle and subtle in practice. A remote exposes modules through a remoteEntry.js manifest. The host declares the remote’s URL and imports the exposed module. The bundler replaces the import with an asynchronous fetch, negotiates shared dependency versions, and returns the module once the remote’s code is available. The subtlety is in the failure modes: a remote that is unreachable crashes the host before React mounts, shared dependency version mismatches cause silent runtime errors, and the singleton configuration is the only thing preventing duplicate React instances from breaking hooks and context .
This chapter covers the Module Federation configuration for host and remote, the shared dependency rules that prevent the most common runtime failures, the error-handling strategies that keep a missing remote from taking down the entire application, and the tooling options that scaffold the architecture. It assumes familiarity with React and a module bundler.
Key point: Module Federation composes applications at runtime. The host loads a remote’s remoteEntry.js, negotiates shared dependencies, and imports the exposed module. React and ReactDOM must be shared as singleton: true with requiredVersion constraints, or the host and remote will run separate React instances and break hooks, context, and state. A remote that is unreachable fails before React mounts, so the error boundary must be at the bootstrap level, not inside the component tree.
Why Module Federation exists as a runtime composition mechanism
A monorepo is not a micro-frontend. Moving multiple applications into one repository with shared tooling solves dependency drift and simplifies CI, but it does not give teams independent deployment. A change to one application still requires rebuilding and redeploying the workspace. Module Federation separates the build artifacts: the remote is built and deployed independently, and the host loads it at runtime by URL .
Shared dependencies must be singletons. If the host bundles React 19 and a remote bundles React 18, the page ends up with two React instances. Hooks fail because the hook dispatcher is per-instance. Context does not propagate across the boundary. The singleton: true configuration tells the federation runtime to use one instance across all containers, and requiredVersion makes a mismatch fail fast rather than produce mysterious runtime errors . The shared surface should be small: React, ReactDOM, and any cross-app communication layer. Everything else can be bundled per application.
The failure mode is a blank page. When a remote is unreachable, the federation runtime fetches the manifest before the host’s bootstrap module runs. If the fetch fails, the runtime throws, the dynamic import('./bootstrap') rejects, and React never mounts. A React error boundary inside the component tree cannot catch this because the tree does not exist yet . The fix is a try/catch or .catch() around the bootstrap import, or a lazy import with a fallback component for optional remotes.
The trade-off. Module Federation adds runtime complexity: version negotiation, network dependency on remote availability, and a build configuration that differs from a standard React app. The alternative—a monorepo with a single deploy—is simpler but sacrifices independent deployment. The architecture is justified when the organizational cost of coordinated releases exceeds the operational cost of runtime federation.
a. Host and remote configuration
A remote exposes modules through the exposes field. The name is the container name used in the host’s import path. The filename is the manifest entry point, conventionally remoteEntry.js. The shared field declares dependencies that the federation runtime should negotiate with the host .
// webpack.config.js (remote)
const { ModuleFederationPlugin } = require('webpack').container;
const deps = require('./package.json').dependencies;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'analytics',
filename: 'remoteEntry.js',
exposes: {
'./Dashboard': './src/Dashboard.tsx',
'./Chart': './src/Chart.tsx',
},
shared: {
react: { singleton: true, requiredVersion: deps.react },
'react-dom': { singleton: true, requiredVersion: deps['react-dom'] },
},
}),
],
};
The host declares the remote URL and imports the exposed module. The import is asynchronous; the bundler replaces it with a call that loads the manifest and negotiates shared dependencies before returning the module.
// webpack.config.js (host)
new ModuleFederationPlugin({
name: 'shell',
remotes: {
analytics: 'analytics@https://cdn.example.com/analytics/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: deps.react },
'react-dom': { singleton: true, requiredVersion: deps['react-dom'] },
},
});
// Host component: lazy-load the remote.
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('analytics/Dashboard'));
export function App() {
return (
<Suspense fallback={<p>Loading dashboard...</p>}>
<Dashboard />
</Suspense>
);
}
The remotes entry can also be dynamic. Module Federation 2.0 supports runtime registration through registerRemotes, which allows the host to add or remove remotes without a rebuild . This is the mechanism for feature flags and canary deployments.
b. Shared dependency rules and the singleton guarantee
The shared configuration is the most consequential part of the federation setup. React and ReactDOM must be singletons with requiredVersion constraints. Without singleton: true, each container loads its own copy. Without requiredVersion, a version mismatch is a warning rather than an error, and the page fails at runtime in ways that are difficult to trace .
// Strict shared configuration.
shared: {
react: {
singleton: true,
requiredVersion: deps.react, // Exact version from host package.json
eager: false, // Lazy-load the shared module
},
'react-dom': {
singleton: true,
requiredVersion: deps['react-dom'],
},
'react-router-dom': {
singleton: true,
requiredVersion: deps['react-router-dom'],
},
}
The requiredVersion in the host should be exact or a narrow range. The remote’s requiredVersion should match the host’s. A remote built against React 19 that loads into a host running React 18 will fail the version check and either fall back to its own copy (if not a singleton) or throw (if a singleton with a strict required version). The failure is the correct behavior: it surfaces the mismatch at the boundary rather than inside a hook call.
The shared surface should be deliberately small. Sharing every dependency recreates the monolith’s version coupling at runtime, with worse debugging. Share React, ReactDOM, and the communication layer (an event bus, a shared context). Let everything else—utility libraries, UI component libraries, date formatting—be bundled per remote . The duplication is measured in kilobytes; the version coupling is measured in deployment coordination.
c. Error handling: the bootstrap boundary and optional remotes
A React error boundary inside the component tree cannot catch a federation bootstrap failure. The manifest fetch happens before createRoot is called. The error boundary component exists in source but is never instantiated . The fix is a plain JavaScript catch around the bootstrap import.
// index.js — catch bootstrap failure before React exists.
import('./bootstrap').catch((error) => {
console.error('Application failed to start:', error);
const root = document.getElementById('root');
if (root) {
root.innerHTML = `
<div style="padding: 2rem; font-family: system-ui;">
<h1>Application failed to load</h1>
<p>Please try refreshing the page.</p>
<pre style="color: red;">${error.message}</pre>
</div>
`;
}
});
For optional remotes—analytics dashboards, secondary features—the React.lazy import can catch the failure and return a fallback component. This keeps the host functional when a non-critical remote is down.
const AnalyticsDashboard = lazy(() =>
import('analytics/Dashboard').catch(() => ({
default: () => <div>Analytics is unavailable.</div>,
}))
);
The catch approach works only if the federation runtime does not throw during shared module negotiation before the lazy import runs. If the remote is listed in the static remotes configuration, the manifest fetch happens at startup regardless of whether the lazy import is invoked. Dynamic registration with registerRemotes defers the fetch until the host decides to load the remote, which is the pattern for optional remotes .
Complete Example Session
// ============================================
// PART 1: REMOTE EXPOSES COMPONENTS
// ============================================
// analytics/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
const deps = require('./package.json').dependencies;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'analytics',
filename: 'remoteEntry.js',
exposes: {
'./Dashboard': './src/Dashboard',
'./Chart': './src/Chart',
},
shared: {
react: { singleton: true, requiredVersion: deps.react },
'react-dom': { singleton: true, requiredVersion: deps['react-dom'] },
},
}),
],
};
// ============================================
// PART 2: HOST DECLARES REMOTE
// ============================================
// shell/webpack.config.js
new ModuleFederationPlugin({
name: 'shell',
remotes: {
analytics: 'analytics@https://cdn.example.com/analytics/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: deps.react },
'react-dom': { singleton: true, requiredVersion: deps['react-dom'] },
},
});
// ============================================
// PART 3: LAZY IMPORT IN HOST COMPONENT
// ============================================
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('analytics/Dashboard'));
export function App() {
return (
<Suspense fallback={<p>Loading...</p>}>
<Dashboard />
</Suspense>
);
}
// ============================================
// PART 4: BOOTSTRAP ERROR HANDLING
// ============================================
// index.js
import('./bootstrap').catch((error) => {
document.getElementById('root').innerHTML =
`<p>Application failed to load: ${error.message}</p>`;
});
// ============================================
// PART 5: OPTIONAL REMOTE FALLBACK
// ============================================
const Analytics = lazy(() =>
import('analytics/Dashboard').catch(() => ({
default: () => <div>Analytics unavailable.</div>,
}))
);
// ============================================
// PART 6: DYNAMIC REMOTE REGISTRATION (MF 2.0)
// ============================================
import { registerRemotes } from '@module-federation/runtime';
registerRemotes([
{
name: 'analytics',
entry: 'https://cdn.example.com/analytics/remoteEntry.js',
},
]);
// ============================================
// PART 7: SHARED VERSION MISMATCH
// ============================================
// Host: React 19 (singleton, requiredVersion: "19.0.0")
// Remote: React 18 (singleton, requiredVersion: "^18.0.0")
// Result: federation runtime throws or falls back.
// Fix: align versions, or remote bundles its own React
// and avoids hooks across the boundary.
// ============================================
// PART 8: VITE EQUIVALENT
// ============================================
// vite.config.ts (remote)
import { federation } from '@module-federation/vite';
export default defineConfig({
plugins: [
federation({
name: 'analytics',
filename: 'remoteEntry.js',
exposes: { './Dashboard': './src/Dashboard.tsx' },
shared: { react: { singleton: true } },
}),
],
});
// ============================================
// PART 9: NX SCAFFOLDING
// ============================================
// npx nx g @nx/react:host shell
// npx nx g @nx/react:remote analytics --host=shell
// Generates consumer/provider with runtime registration.
// ============================================
// PART 10: THE COMPLETE FLOW
// ============================================
// 1. Remote exposes modules via remoteEntry.js.
// 2. Host declares remote URL and imports module.
// 3. Federation runtime fetches manifest.
// 4. Shared dependencies negotiated as singletons.
// 5. Module returned; host renders it lazily.
// 6. Bootstrap failure caught in plain JS.
// 7. Optional remotes use lazy catch fallback.
// 8. Dynamic registration for feature flags.
Quick Reference
Configuration Fields
| Field | Container | Purpose |
|---|---|---|
name | Both | Container name for imports |
filename | Remote | Manifest entry (remoteEntry.js) |
exposes | Remote | Modules available to host |
remotes | Host | Remote URLs and aliases |
shared | Both | Singleton dependency negotiation |
Shared Dependency Rules
| Package | Configuration | Reason |
|---|---|---|
react | singleton: true, requiredVersion | Hooks and context require one instance |
react-dom | singleton: true, requiredVersion | DOM renderer must match React |
react-router-dom | singleton: true | Router context must be shared |
| Everything else | Bundled per app | Avoid version coupling |
Error Handling Levels
| Level | Catches | Implementation |
|---|---|---|
| Bootstrap | Manifest fetch failure | import('./bootstrap').catch() |
| Lazy import | Remote module load failure | lazy(() => import().catch()) |
| Component | Render errors inside remote | React error boundary |
Tooling Options
| Tool | Approach | Setup |
|---|---|---|
| Webpack 5 | Native ModuleFederationPlugin | Manual config |
| Vite | @module-federation/vite | Plugin config |
| Nx | Generators with runtime registration | nx g @nx/react:remote |
| Rspack | ModuleFederationPlugin | Manual config |
Best Practices
✅ Do This:
// Share React as a singleton with exact version. // ✅
react: { singleton: true, requiredVersion: deps.react }
// Catch bootstrap failure in plain JavaScript. // ✅
import('./bootstrap').catch(renderError);
// Lazy-load remote with fallback for optional features. // ✅
lazy(() => import('analytics/Dashboard').catch(fallback))
// Keep shared surface minimal. // ✅
// React, ReactDOM, communication layer only.
// Use dynamic registration for feature-flagged remotes. // ✅
registerRemotes([...])
❌ Don’t Do This:
// Omit singleton for React. // ❌
react: {} // Duplicate React instances; hooks break.
// Rely on error boundary for bootstrap failure. // ❌
// React never mounts; boundary never catches.
// Share every dependency. // ❌
// Recreates monolith coupling at runtime.
// Use hyphenated container names. // ❌
name: 'my-remote' // Invalid JS identifier; webpack error.
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Blank page when remote down | Bootstrap failure before React mounts | import('./bootstrap').catch() |
| Hooks error across boundary | Duplicate React instances | singleton: true |
| Context not shared | Separate React trees | Singleton + shared context |
| Version mismatch warning | requiredVersion missing | Add exact version constraint |
| Remote assets 404 | publicPath wrong for nested routes | Set publicPath: '/' or dynamic |
| Container name error | Hyphen in name | Use camelCase |
Real-World Examples
1. Remote Exposes Component
exposes: { './Dashboard': './src/Dashboard.tsx' }
2. Host Declares Remote
remotes: { analytics: 'analytics@https://cdn.example.com/remoteEntry.js' }
3. Singleton React
shared: { react: { singleton: true, requiredVersion: deps.react } }
4. Bootstrap Catch
import('./bootstrap').catch(renderError);
5. Lazy Remote with Fallback
lazy(() => import('analytics/Dashboard').catch(() => ({ default: Fallback })))
6. Vite Federation Plugin
import { federation } from '@module-federation/vite';
federation({ name: 'remote', exposes: { ... }, shared: { react: { singleton: true } } })
7. Nx Generator
npx nx g @nx/react:remote analytics --host=shell
8. Dynamic Registration
registerRemotes([{ name: 'analytics', entry: '...' }]);
9. Shared Router
'react-router-dom': { singleton: true, requiredVersion: deps['react-router-dom'] }
10. Exact Host Version
version: "19.0.0" // Not "^19.0.0" for shared host
Visual: Host and Remote Composition
┌──────────────────────────────────────────────┐
│ HOST (shell) │
│ │
│ remotes: { analytics: '...remoteEntry.js' } │
│ shared: { react: singleton } │
│ │ │
│ │ lazy import │
│ ▼ │
│ ┌────────────────────────────────────┐ │
│ │ Federation Runtime │ │
│ │ 1. Fetch remoteEntry.js │ │
│ │ 2. Negotiate shared React │ │
│ │ 3. Load remote module │ │
│ └────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────┐ │
│ │ REMOTE (analytics) │ │
│ │ exposes: './Dashboard' │ │
│ │ shared: react (singleton) │ │
│ └────────────────────────────────────┘ │
│ │
│ One React instance. Hooks work. │
│ │
└──────────────────────────────────────────────┘
Failure Timeline
┌──────────────────────────────────────────────┐
│ REMOTE DOWN │
│ │
│ Browser loads host HTML/JS │
│ │ │
│ ▼ │
│ Federation runtime fetches manifest │
│ │ │
│ ▼ │
│ ERR_CONNECTION_REFUSED │
│ │ │
│ ▼ │
│ Dynamic import rejects │
│ │ │
│ ▼ │
│ createRoot NEVER runs │
│ │ │
│ ▼ │
│ Error boundary NEVER mounts │
│ │ │
│ ▼ │
│ Blank page. │
│ │
│ Fix: catch at bootstrap level. │
│ │
└──────────────────────────────────────────────┘
Shared Dependency Negotiation
┌──────────────────────────────────────────────┐
│ VERSION NEGOTIATION │
│ │
│ Host: react@19.0.0 (singleton) │
│ Remote: requiredVersion "^19.0.0" │
│ │ │
│ ├── Compatible → share host's React │
│ │ │
│ └── Incompatible → throw or fallback │
│ │
│ Without requiredVersion: │
│ Warning only. Runtime failure later. │
│ │
│ Without singleton: │
│ Two React instances. Hooks break. │
│ │
└──────────────────────────────────────────────┘
Error Handling Levels
┌──────────────────────────────────────────────┐
│ ERROR HANDLING │
│ │
│ Bootstrap (plain JS): │
│ import('./bootstrap').catch() │
│ Catches manifest fetch failure. │
│ │
│ Lazy import (React.lazy): │
│ lazy(() => import().catch()) │
│ Catches module load failure. │
│ Renders fallback component. │
│ │
│ Error boundary (React): │
│ Catches render errors. │
│ Does NOT catch bootstrap failure. │
│ │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Remote entry | remoteEntry.js manifest |
| Host declaration | remotes: { alias: 'name@url' } |
| Exposure | exposes: { './Module': './src/Module' } |
| React sharing | singleton: true, requiredVersion |
| Bootstrap failure | Catch at import('./bootstrap') |
| Optional remote | lazy(() => import().catch(fallback)) |
| Shared surface | React, ReactDOM, communication layer only |
| Container name | camelCase, valid JS identifier |
| Dynamic registration | registerRemotes (MF 2.0) |
Key takeaways:
- Module Federation composes applications at runtime. The host loads the remote’s manifest, negotiates shared dependencies, and imports the exposed module. The remote is built and deployed independently of the host .
- React must be a singleton with a required version. Two React instances break hooks, context, and state. The
singleton: trueandrequiredVersionconfiguration ensures one instance and fails fast on mismatch . - The shared surface should be minimal. Share React, ReactDOM, and the communication layer. Everything else can be bundled per remote. Sharing everything recreates the monolith’s coupling at runtime with worse debugging .
- Bootstrap failures cannot be caught by React error boundaries. The manifest fetch happens before
createRootruns. Catch the failure at theimport('./bootstrap')level with plain JavaScript . - Optional remotes need lazy imports with fallbacks. A
React.lazyimport wrapped in.catch()returns a fallback component when the remote is unavailable, keeping the rest of the host functional . - The failure modes are version mismatches and unreachable remotes. Both are addressed by configuration: strict
requiredVersionfor shared dependencies, and bootstrap-level error handling for network failures. - Tooling scaffolds the architecture, not the governance. Nx, Vite plugins, and CLI generators produce the configuration. The shared dependency policy, the error-handling strategy, and the decision about which remotes are optional are organizational decisions that the tooling cannot make.
Remember: Module Federation is a runtime composition mechanism that lets a host load components from independently deployed remotes. The remote exposes modules through a manifest; the host imports them lazily; the federation runtime negotiates shared dependencies so React exists once. The architecture solves the team-coordination problem of a large frontend, but it introduces new failure modes: a remote that is unreachable crashes the host before React mounts, and a version mismatch in a shared dependency produces silent runtime errors. The configuration that prevents these failures is small: singleton: true and requiredVersion for React and ReactDOM, a bootstrap-level catch for the manifest fetch, and a lazy import with a fallback for optional remotes. Get the shared dependency rules right, and the federation is stable. Get them wrong, and the page is blank and the console is the only clue.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!