In enterprise ecosystems with dozens of distributed product engineering teams, monolithic SPA repositories inevitably become critical organizational bottlenecks: CI/CD build durations degrade into tens of minutes, deployment cycles become interlocked by cross-team dependencies, and scaling codebases leads to persistent Git merge conflicts.
The microfrontend architecture addresses this challenge by decomposing a monolithic user interface into independent, autonomously developed, and independently deployable subsystems. Historically, isolation attempts suffered from fatal architectural flaws:
Module Federation resolves these limitations by enabling seamless runtime integration of remote modules without rebuilding the host container.
The Module Federation v2 specification (implemented in the Rspack / Rsbuild ecosystem via @module-federation/enhanced) elevates microfrontend architecture to modern enterprise performance:
@module-federation/runtime) for intercepting chunk loading, injecting authorization headers, and enabling dynamic Service Discovery.In Module Federation architecture, applications operate within two foundational roles:
An application can simultaneously act as both a Host and a Remote (bi-directional federation).
┌──────────────────────────────────────────────────────────────────────────────────┐
│ HOST APPLICATION (Shell) │
│ http://app.enterprise.com │
│ │
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
│ │ Global Context Providers: AuthContext, QueryClientProvider, ThemeProvider │ │
│ └──────────────────────────────────────┬─────────────────────────────────────┘ │
│ │ │
│ ┌──────────────────────────────────────▼─────────────────────────────────────┐ │
│ │ Module Federation v2 Runtime │ │
│ │ (Singleton Registry: [email protected], [email protected]) │ │
│ └──────────────────┬───────────────────────────────────┬─────────────────────┘ │
│ │ │ │
└─────────────────────┼───────────────────────────────────┼────────────────────────┘
│ (fetch remoteEntry.js) │ (fetch remoteEntry.js)
▼ ▼
┌───────────────────────────────────────┐ ┌──────────────────────────────────────┐
│ REMOTE 1: Analytics Dashboard │ │ REMOTE 2: Billing & Checkout │
│ http://analytics.cdn.internal │ │ http://billing.cdn.internal │
│ Exposes: './DashboardWidget' │ │ Exposes: './CheckoutFlow' │
│ Shared: react, react-dom │ │ Shared: react, react-dom │
└───────────────────────────────────────┘ └──────────────────────────────────────┘The central technical hurdle in a distributed runtime is preventing library duplication across the Shared Scope. If the Host and Remote each load their own copies of react or react-dom, React 19 throws a fatal hook context error (Invalid hook call: Multiple React instances detected).
Scaffold the Remote micro-app using @rsbuild/plugin-react and @module-federation/rsbuild-plugin.
Define exposed component entry points and configure react and react-dom as strict singletons.
Produce the production build containing deterministic dependency and chunk metadata.
Upload Remote build artifacts to an isolated storage bucket with appropriate CORS headers.
Import the remote module asynchronously via React.lazy with isolated fault tolerance.
Rsbuild leverages the Rust-based Rspack engine and provides native, first-class Module Federation v2 support:
import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';
import { pluginModuleFederation } from '@module-federation/rsbuild-plugin';
export default defineConfig({
plugins: [
pluginReact(),
pluginModuleFederation({
name: 'analytics_remote',
filename: 'remoteEntry.js',
// Components exposed to consumer host applications
exposes: {
'./DashboardWidget': './src/widgets/DashboardWidget.tsx',
'./MetricsSummary': './src/widgets/MetricsSummary.tsx',
},
// Shared singleton dependency contracts
shared: {
react: {
singleton: true,
requiredVersion: '^19.0.0',
},
'react-dom': {
singleton: true,
requiredVersion: '^19.0.0',
},
},
}),
],
server: {
port: 3001,
},
});import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';
import { pluginModuleFederation } from '@module-federation/rsbuild-plugin';
export default defineConfig({
plugins: [
pluginReact(),
pluginModuleFederation({
name: 'host_shell',
// Declare remote microfrontends via their manifests
remotes: {
analytics_remote: 'analytics_remote@http://localhost:3001/mf-manifest.json',
},
shared: {
react: {
singleton: true,
eager: true, // Host requires React synchronously at initial boot
requiredVersion: '^19.0.0',
},
'react-dom': {
singleton: true,
eager: true,
requiredVersion: '^19.0.0',
},
},
}),
],
server: {
port: 3000,
},
});To guarantee type-safe imports for federated components within the Host application, configure ambient module declarations:
declare module 'analytics_remote/DashboardWidget' {
import type { FC } from 'react';
export interface DashboardWidgetProps {
tenantId: string;
refreshIntervalMs?: number;
onMetricSelect?: (metricId: string) => void;
}
const DashboardWidget: FC<DashboardWidgetProps>;
export default DashboardWidget;
}Network glitches, CDN timeouts, or HTTP 404/500 errors when fetching remoteEntry.js must never bring down the primary Shell application. Every microfrontend integration must be isolated behind a resilient Error Boundary:
User navigates to /analytics. React Router triggers the Lazy container component.
Runtime requests mf-manifest.json from the remote CDN and evaluates chunk hashes.
Remote module binds to Host React 19 singleton instance without re-downloading core packages.
On network failure or uncaught remote exception, Host renders a localized fallback UI widget.
What critical advantage does mf-manifest.json provide in Module Federation v2 compared to raw remoteEntry.js scripts in v1?
Here is a production-grade remote module loader combining React 19 Suspense, a custom ErrorBoundary, automated exponential backoff retries for CDN hiccups, and typed prop forwarding:
import React, { Component, Suspense, lazy, type ReactNode } from 'react';
interface ErrorBoundaryProps {
fallback: (error: Error, reset: () => void) => ReactNode;
children: ReactNode;
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
export class MicrofrontendErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
public override state: ErrorBoundaryState = {
hasError: false,
error: null,
};
public static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
public override componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
console.error('[Microfrontend Crash Alert]:', error, errorInfo);
}
private handleReset = (): void => {
this.setState({ hasError: false, error: null });
};
public override render(): ReactNode {
if (this.state.hasError && this.state.error) {
return this.props.fallback(this.state.error, this.handleReset);
}
return this.props.children;
}
}
// Dynamic import utility with automatic retry on transient CDN network failures
export function retryDynamicImport<T>(
importFn: () => Promise<T>,
retriesLeft = 3,
intervalMs = 1000
): Promise<T> {
return new Promise((resolve, reject) => {
importFn()
.then(resolve)
.catch((error: unknown) => {
if (retriesLeft <= 1) {
reject(error instanceof Error ? error : new Error(String(error)));
return;
}
setTimeout(() => {
retryDynamicImport(importFn, retriesLeft - 1, intervalMs * 1.5)
.then(resolve)
.catch(reject);
}, intervalMs);
});
});
}
interface FederatedLoaderProps<TProps extends Record<string, unknown>> {
load: () => Promise<{ default: React.ComponentType<TProps> }>;
props: TProps;
moduleName: string;
fallbackUi?: ReactNode;
}
export function FederatedComponentLoader<TProps extends Record<string, unknown>>({
load,
props,
moduleName,
fallbackUi,
}: FederatedLoaderProps<TProps>): React.JSX.Element {
const LazyComponent = React.useMemo(() => {
return lazy(() => retryDynamicImport(load));
}, [load]);
return (
<MicrofrontendErrorBoundary
fallback={(error, reset) => (
<div className="p-6 rounded-xl border border-danger/30 bg-danger/5 text-text-primary flex flex-col gap-3">
<div className="flex items-center gap-2 text-danger font-bold">
<span>⚠️ Microfrontend Load Failure: {moduleName}</span>
</div>
<p className="text-sm text-text-muted">{error.message}</p>
<button
type="button"
onClick={reset}
className="self-start px-4 py-2 rounded-lg bg-bg-elevated border border-border hover:border-accent-cyan text-xs font-semibold text-accent-cyan transition-colors"
>
Retry Remote Connection
</button>
</div>
)}
>
<Suspense
fallback={
fallbackUi || (
<div className="p-8 rounded-xl border border-border bg-bg-surface flex items-center justify-center">
<div className="flex items-center gap-3 text-accent-cyan text-sm font-mono animate-pulse">
<span className="w-2 h-2 rounded-full bg-accent-cyan" />
Initializing remote service {moduleName}...
</div>
</div>
)
}
>
<LazyComponent {...props} />
</Suspense>
</MicrofrontendErrorBoundary>
);
}If Host and Remote configurations omit singleton: true for react and react-dom, each Remote will load its own instance of React into memory. This breaks the React Context API (returning default fallback values) and causes hook dispatcher exceptions.
// ❌ Anti-pattern: Missing singleton instantiates duplicate React cores in memory
shared: {
react: { requiredVersion: '^19.0.0' },
'react-dom': { requiredVersion: '^19.0.0' },
}
// ✅ Production Pattern: Strict singleton with required semantic versioning
shared: {
react: {
singleton: true,
requiredVersion: '^19.0.0',
strictVersion: true,
},
'react-dom': {
singleton: true,
requiredVersion: '^19.0.0',
strictVersion: true,
},
}When Host and Remote share traditional global CSS or un-prefixed Tailwind CSS setups, styling definitions from one remote can leak and unintentionally override the Host Shell.
/* ❌ Anti-pattern: Global button tag styling in Remote */
button {
background-color: #ef4444; /* Overrides all button elements across the host! */
}
/* ✅ Production Pattern: Tailwind CSS v4 custom theme prefixes or Scoped CSS Modules */
@theme prefix(mf-analytics) {
/* All utility classes isolated under custom namespace */
}If a Host initializes synchronously while React is configured as an asynchronous shared dependency without eager: true, the app crashes on boot with Shared module is not available for eager consumption.
Remedy: Use an asynchronous entry bootstrap pattern (import('./bootstrap')) or configure the Host shared entry with eager: true.