Micro-frontends let multiple teams build and deploy independently. Module Federation makes this possible at runtime.
Architecture Overview
┌─────────────────────────────────────┐
│ Shell Application │
│ ┌─────────┐ ┌──────┐ ┌──────────┐ │
│ │ Header │ │ Nav │ │ Footer │ │
│ └─────────┘ └──────┘ └──────────┘ │
│ ┌──────────────────────────────┐ │
│ │ Remote App Container │ │
│ │ ┌────────┐ ┌────────────┐ │ │
│ │ │ Team A │ │ Team B │ │ │
│ │ │Products│ │ Checkout │ │ │
│ │ └────────┘ └────────────┘ │ │
│ └──────────────────────────────┘ │
└─────────────────────────────────────┘
Install Module Federation Plugin
pnpm add @module-federation/nextjs-mf webpack
Shell Application Config
// next.config.js (Shell)
const NextFederationPlugin =
require("@module-federation/nextjs-mf").NextFederationPlugin;
/** @type {import('next').NextConfig} */
const nextConfig = {
webpack(config, { isServer }) {
config.plugins.push(
new NextFederationPlugin({
name: "shell",
filename: "static/chunks/remoteEntry.js",
remotes: {
products: `products@${
process.env.PRODUCTS_URL ?? "http://localhost:3001"
}/_next/static/${isServer ? "ssr" : "chunks"}/remoteEntry.js`,
checkout: `checkout@${
process.env.CHECKOUT_URL ?? "http://localhost:3002"
}/_next/static/${isServer ? "ssr" : "chunks"}/remoteEntry.js`,
},
shared: {
react: { singleton: true, eager: true },
"react-dom": { singleton: true, eager: true },
},
})
);
return config;
},
};
module.exports = nextConfig;
Remote Application Config
// next.config.js (Products remote)
const NextFederationPlugin =
require("@module-federation/nextjs-mf").NextFederationPlugin;
/** @type {import('next').NextConfig} */
const nextConfig = {
webpack(config) {
config.plugins.push(
new NextFederationPlugin({
name: "products",
filename: "static/chunks/remoteEntry.js",
exposes: {
"./ProductList": "./components/ProductList",
"./ProductDetail": "./components/ProductDetail",
"./ProductSearch": "./components/ProductSearch",
},
shared: {
react: { singleton: true, eager: true },
"react-dom": { singleton: true, eager: true },
},
})
);
return config;
},
};
module.exports = nextConfig;
Loading Remote Components
// components/RemoteComponent.tsx
"use client";
import { Suspense, lazy, type ComponentType } from "react";
interface RemoteComponentProps {
remote: string;
module: string;
fallback?: React.ReactNode;
[key: string]: unknown;
}
function loadRemote(remote: string, module: string): ComponentType<any> {
return lazy(async () => {
const container = (window as any)[remote];
if (!container) {
await new Promise<void>((resolve, reject) => {
const script = document.createElement("script");
script.src = `${getRemoteUrl(remote)}/_next/static/chunks/remoteEntry.js`;
script.onload = () => resolve();
script.onerror = () => reject(new Error(`Failed to load remote: ${remote}`));
document.head.appendChild(script);
});
}
await (window as any)[remote].init(
Object.assign(
{ react: { get: () => Promise.resolve(() => require("react")) } },
__webpack_share_scopes__.default
)
);
const factory = await (window as any)[remote].get(module);
const Module = factory();
return { default: Module.default ?? Module };
});
}
function getRemoteUrl(remote: string): string {
const urls: Record<string, string> = {
products: process.env.NEXT_PUBLIC_PRODUCTS_URL ?? "http://localhost:3001",
checkout: process.env.NEXT_PUBLIC_CHECKOUT_URL ?? "http://localhost:3002",
};
return urls[remote] ?? "";
}
export function RemoteComponent({
remote,
module,
fallback,
...props
}: RemoteComponentProps) {
const Component = loadRemote(remote, module);
return (
<Suspense fallback={fallback ?? <div className="animate-pulse h-48 bg-muted rounded" />}>
<Component {...props} />
</Suspense>
);
}
Shared State Between Micro-Frontends
// lib/shared-store.ts
type Listener = () => void;
class SharedEventBus {
private listeners = new Map<string, Set<Listener>>();
private state = new Map<string, unknown>();
emit(event: string, data: unknown) {
this.state.set(event, data);
this.listeners.get(event)?.forEach((fn) => fn());
}
subscribe(event: string, listener: Listener): () => void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(listener);
return () => this.listeners.get(event)?.delete(listener);
}
getState<T>(event: string): T | undefined {
return this.state.get(event) as T | undefined;
}
}
// Singleton attached to window for cross-app sharing
const GLOBAL_KEY = "__SHARED_EVENT_BUS__";
export function getSharedBus(): SharedEventBus {
if (typeof window === "undefined") return new SharedEventBus();
if (!(window as any)[GLOBAL_KEY]) {
(window as any)[GLOBAL_KEY] = new SharedEventBus();
}
return (window as any)[GLOBAL_KEY];
}
Usage in Shell
// app/products/page.tsx
import { RemoteComponent } from "@/components/RemoteComponent";
export default function ProductsPage() {
return (
<main className="container py-8">
<h1 className="text-3xl font-bold mb-6">Products</h1>
<RemoteComponent
remote="products"
module="./ProductList"
fallback={<p>Loading products...</p>}
category="all"
/>
</main>
);
}
Need to Scale Your Frontend Architecture?
We design and implement micro-frontend systems for large teams and complex products. Reach out to discuss your architecture needs.