Skip to main content
Back to Blog
Tutorials
2 min read
December 19, 2024

How to Build Micro-Frontends with Module Federation in Next.js

Implement a micro-frontend architecture using Webpack Module Federation with Next.js, enabling independent team deployments and shared components.

Ryel Banfield

Founder & Lead Developer

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.

micro-frontendsmodule federationNext.jsarchitecturetutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles