Skip to main content

Next.js Module Federation

Introduction

Module Federation is an advanced feature introduced in Webpack 5 that allows JavaScript applications to dynamically share code and dependencies. In the context of Next.js, Module Federation enables developers to create micro-frontends - separate applications that can work together while being developed and deployed independently.

This approach solves several common challenges in large-scale application development:

  • Team Autonomy: Different teams can work on different parts of the application independently
  • Independent Deployments: Each micro-frontend can be deployed separately without affecting the entire system
  • Incremental Upgrades: Applications can be upgraded piece by piece
  • Code Sharing: Runtime sharing of components and logic between applications

In this guide, we'll explore how to implement Module Federation in Next.js applications, understand its benefits and limitations, and see real-world examples.

Prerequisites

Before diving into Module Federation with Next.js, ensure you have:

  • Basic understanding of Next.js
  • Familiarity with Webpack concepts
  • Next.js 10+ installed (which uses Webpack 5)
  • Node.js 12+ installed

Setting Up Module Federation in Next.js

Step 1: Install Required Dependencies

First, we need to install the necessary packages:

bash
npm install @module-federation/nextjs-mf

Step 2: Configure the Host Application

The host application is the main application that will consume modules from remote applications. Create or modify your next.config.js file:

js
const { NextFederationPlugin } = require('@module-federation/nextjs-mf');

module.exports = {
webpack(config, options) {
// Enable the plugin only in client-side builds
if (!options.isServer) {
config.plugins.push(
new NextFederationPlugin({
name: 'host',
remotes: {
remote1: 'remote1@http://localhost:3001/_next/static/chunks/remoteEntry.js',
},
shared: {
react: {
singleton: true,
requiredVersion: false,
},
'react-dom': {
singleton: true,
requiredVersion: false,
},
},
})
);
}

return config;
},
};

Step 3: Configure the Remote Application

The remote application is the one that exposes modules to be consumed by the host. Set up its next.config.js:

js
const { NextFederationPlugin } = require('@module-federation/nextjs-mf');

module.exports = {
webpack(config, options) {
// Enable the plugin only in client-side builds
if (!options.isServer) {
config.plugins.push(
new NextFederationPlugin({
name: 'remote1',
filename: 'static/chunks/remoteEntry.js',
exposes: {
'./Button': './components/Button.js',
'./Header': './components/Header.js',
},
shared: {
react: {
singleton: true,
requiredVersion: false,
},
'react-dom': {
singleton: true,
requiredVersion: false,
},
},
})
);
}

return config;
},
};

Step 4: Create Components to Expose

In the remote application, create components that you want to expose. For example, components/Button.js:

jsx
// components/Button.js in remote application
import React from 'react';

const Button = ({ text, onClick }) => {
return (
<button
onClick={onClick}
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
{text || 'Click me'}
</button>
);
};

export default Button;

Step 5: Consume Remote Components

In the host application, create a dynamic import to use the remote component:

jsx
// pages/example.js in host application
import React from 'react';
import dynamic from 'next/dynamic';

// Dynamic import with error handling and loading state
const RemoteButton = dynamic(
() => import('remote1/Button')
.catch(err => {
console.error('Error loading remote component:', err);

// Fallback component if remote fails to load
return () => <button className="fallback-button">Fallback Button</button>;
}),
{
loading: () => <p>Loading remote button...</p>,
ssr: false // Disable server-side rendering for federated modules
}
);

export default function ExamplePage() {
return (
<div className="container mx-auto p-4">
<h1>Module Federation Example</h1>
<div className="my-4">
<RemoteButton
text="I'm a remote button!"
onClick={() => alert('Hello from remote app!')}
/>
</div>
</div>
);
}

Understanding the Configuration

Let's break down the key parts of the Module Federation configuration:

  1. name: The unique identifier for the application in the federation
  2. remotes: References to external federated modules (used in the host)
  3. exposes: Components/modules to be exposed to other applications (used in the remote)
  4. filename: The name of the remote entry file
  5. shared: Dependencies that should be shared between host and remotes

The shared configuration is particularly important as it prevents duplicate libraries (like React) from being loaded multiple times.

Advanced Usage Patterns

1. Dynamic Remote URLs

In production environments, you might need to load remote modules from different URLs based on environment:

js
const remotes = {
development: {
remote1: 'remote1@http://localhost:3001/_next/static/chunks/remoteEntry.js',
},
production: {
remote1: 'remote1@https://production.example.com/_next/static/chunks/remoteEntry.js',
},
};

// In next.config.js
new NextFederationPlugin({
name: 'host',
remotes: remotes[process.env.NODE_ENV],
// ...rest of configuration
});

2. Bidirectional Federation

Applications can both consume and expose modules at the same time:

js
// In next.config.js
new NextFederationPlugin({
name: 'app1',
remotes: {
app2: 'app2@http://localhost:3002/_next/static/chunks/remoteEntry.js',
},
exposes: {
'./Navigation': './components/Navigation.js',
},
shared: {
// Shared dependencies
},
});

3. Error Handling for Remote Modules

When a remote module fails to load, provide fallbacks:

jsx
const RemoteFeature = dynamic(
() => import('remote1/Feature').catch(() => () => <LocalFeatureFallback />),
{
ssr: false,
loading: () => <LoadingComponent />
}
);

Real-World Application Example

Let's look at a real-world example where Module Federation provides significant value - a large e-commerce platform split into micro-frontends:

  1. Main Shell App: Core navigation and layout (Host)
  2. Product Catalog App: Product listings and search (Remote)
  3. User Account App: User profile and settings (Remote)
  4. Shopping Cart App: Cart management (Remote)

Main Shell App Configuration

js
// next.config.js for the Main Shell
const { NextFederationPlugin } = require('@module-federation/nextjs-mf');

module.exports = {
webpack(config, options) {
if (!options.isServer) {
config.plugins.push(
new NextFederationPlugin({
name: 'shell',
remotes: {
catalog: 'catalog@/catalog/remoteEntry.js',
account: 'account@/account/remoteEntry.js',
cart: 'cart@/cart/remoteEntry.js',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
'styled-components': { singleton: true },
},
})
);
}
return config;
},
};

Main Shell App Implementation

jsx
// pages/index.js in the Main Shell
import React from 'react';
import dynamic from 'next/dynamic';
import Layout from '../components/Layout';

const ProductList = dynamic(
() => import('catalog/ProductList'),
{ ssr: false, loading: () => <p>Loading products...</p> }
);

const CartSummary = dynamic(
() => import('cart/Summary'),
{ ssr: false, loading: () => <p>Loading cart...</p> }
);

const UserProfile = dynamic(
() => import('account/Profile'),
{ ssr: false, loading: () => <p>Loading user profile...</p> }
);

export default function HomePage() {
return (
<Layout>
<div className="grid grid-cols-12 gap-4">
<div className="col-span-8">
<h2 className="text-2xl font-bold mb-4">Featured Products</h2>
<ProductList featured={true} maxItems={6} />
</div>
<div className="col-span-4">
<UserProfile compact={true} />
<div className="mt-6">
<CartSummary />
</div>
</div>
</div>
</Layout>
);
}

Benefits of This Approach

  1. Team Autonomy: The catalog team can update product listings without coordinating with other teams
  2. Performance: Each micro-frontend can be optimized independently
  3. Progressive Updates: The cart functionality can be upgraded without affecting the catalog or account sections
  4. Specialized Teams: Teams can focus on their domain expertise (e.g., checkout flows, product discovery)

Common Challenges and Solutions

1. SSR Limitations

Module Federation in Next.js currently has limitations with Server-Side Rendering:

Challenge: Federated modules cannot be properly server-rendered.

Solution: Use the ssr: false option with dynamic imports and implement good loading states:

jsx
const RemoteComponent = dynamic(
() => import('remote/Component'),
{
ssr: false,
loading: () => <DetailedLoadingState />
}
);

2. Versioning and Compatibility

Challenge: Ensuring remote modules remain compatible with the host.

Solution: Implement contract testing between hosts and remotes, and consider versioning your exposed modules:

js
// Remote config
exposes: {
'./Button-v1': './components/Button-v1.js',
'./Button-v2': './components/Button-v2.js',
}

// Host usage
import ButtonV1 from 'remote/Button-v1';

3. Styling Conflicts

Challenge: CSS conflicts between host and remote applications.

Solution: Use CSS-in-JS solutions or CSS Modules to scope styles properly:

jsx
// In remote component
import styles from './Button.module.css';

const Button = () => <button className={styles.button}>Click me</button>;

Performance Optimization

To ensure your federated Next.js application performs well:

  1. Optimize Shared Dependencies: Be strategic about what you share
  2. Implement Code Splitting: Don't load all remote modules at once
  3. Add Preload Hints: Preload critical remote entries

Example for preloading remote entries:

jsx
// _document.js in host application
import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
render() {
return (
<Html>
<Head>
<link
rel="preload"
href="http://localhost:3001/_next/static/chunks/remoteEntry.js"
as="script"
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}

export default MyDocument;

Summary

Module Federation in Next.js enables powerful micro-frontend architectures that allow teams to work independently while sharing code at runtime. Key takeaways include:

  • Module Federation allows sharing components and logic between Next.js applications
  • Configuration involves setting up host and remote applications with the NextFederationPlugin
  • Remote components are imported dynamically with error handling and fallbacks
  • SSR has limitations with federated modules, requiring client-side rendering
  • Real-world applications benefit from team autonomy and independent deployment cycles

By implementing Module Federation in your Next.js applications, you can build more maintainable, scalable frontend architectures that enable teams to deliver features faster with less coordination overhead.

Additional Resources

Here are some resources to deepen your understanding of Module Federation in Next.js:

Exercises

  1. Set up a basic host and remote Next.js application using Module Federation
  2. Create a remote component with props and event handlers, then consume it in the host
  3. Implement error handling for when a remote module fails to load
  4. Create a bidirectional federation where two applications consume modules from each other
  5. Build a small micro-frontend architecture with three applications: a shell, a product module, and a user module


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)