Next.js & Module Federation

Project Deprecation

Support for Next.js is ending read more

Next 12Next 13Next 14Next 15SSR (Pages Router) App Router Not Reccomended

Demo Reference

Check out the example project list here: Next.js SSR

Setup Environment

Before getting started, you will need to install Node.js, and ensure that your Node.js version >= 16. We recommend using the LTS version of Node.js 20.

You can check the currently used Node.js version with the following command:

node -v

If you do not have Node.js installed in your current environment, or the installed version is too low, you can use nvm or fnm to install the required version.

Here is an example of how to install the Node.js 20 LTS version via nvm:

# Install the long-term support version of Node.js 20
nvm install 20 --lts

# Make the newly installed Node.js 20 as the default version
nvm alias default 20

# Switch to the newly installed Node.js 20
nvm use 20

Step 1: Setup Next Applications

Create Next Project

You can use create-next-app to create a next project. Just execute the following command:

npm
yarn
pnpm
bun
npx create-next-app@latest

Create App 1

npx create-next-app@latest

"What is your project named?":
> mfe1

"Would you like to use App Router?":
> No

Create App 2

npx create-next-app@latest

"What is your project named?":
> mfe2

"Would you like to use App Router?":
> No

Install

cd mfe1
pnpm add @module-federation/nextjs-mf webpack -D
pnpm i
cd mfe2
pnpm add @module-federation/nextjs-mf webpack -D
pnpm i

Existing Projects

npm
yarn
pnpm
bun
npm i @module-federation/nextjs-mf webpack -D
next.config.mjs
import { NextFederationPlugin } from '@module-federation/nextjs-mf';

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  webpack(config,options ){
    config.plugins.push(
      new NextFederationPlugin({
        name: 'mfe1',
        filename: 'static/chunks/remoteEntry.js',
        remotes: {
          mfe2: `http://localhost:3001/static/${options.isServer ? 'ssr' : 'chunks'}/remoteEntry.js`,
        },
        shared: {},
        extraOptions: {
          exposePages: true,
          enableImageLoaderFix: true,
          enableUrlLoaderFix: true,
        },
      })
    )
    return config
  }
};

export default nextConfig;

Step 2: Override Webpack

Set Local Webpack Env

"Local Webpack" means you must have webpack installed as a dependency, and next will not use its bundled copy of webpack which cannot be used as it does not export all of Webpacks internals

shell
cross-env NEXT_PRIVATE_LOCAL_WEBPACK=true next dev
# or
cross-env NEXT_PRIVATE_LOCAL_WEBPACK=true next build

.env can be set as well, but can be unreliable in setting NEXT_PRIVATE_LOCAL_WEBPACK in time.

.env
NEXT_PRIVATE_LOCAL_WEBPACK=true

Ensure Webpack is installed manually

Webpack must be installed, otherwise the build will throw MODULE_NOT_FOUND errors

npm
yarn
pnpm
bun
npm i webpack -D

Step 3: Implementing SSR

Add Server Lifecycle

To ensure that Next.js creates a server runtime, _document must implement either getInitialProps or getServerSideProps

Without a server lifecycle method, next will attempt to SSG pages it believes are static.

Without a server runtime, there is no server to render updates remotes

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

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx);
    return initialProps;
  }

  render() {
    return (
      <Html>
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

Add Revalidation to Hot Reload Node

To handle updates in remote modules, the revalidate function is used. This is because Webpack uses a chunk cache and won't fetch an already loaded chunk, and a warm server won't recognize updates automatically.

There are two primary ways to implement revalidation:

  • Render Blocking
  • Stale While Revalidate
Render Blocking
Stale While Revalidate

This implementation is recommended for most use cases as it helps avoid hydration errors by ensuring that the server and client are always in sync. By blocking and checking for updates before rendering, you can guarantee that your application is always up-to-date without negatively impacting the user experience.

How it Works:

  • Before rendering the page, the server checks if there are any updates available.
  • If updates are available, it proceeds with Hot Module Replacement (HMR) before responding to the client request.
  • This method ensures that all users receive the latest version of the application without encountering inconsistencies between the server-rendered and client-rendered content.

Implementation Example:

_document
import { revalidate } from '@module-federation/nextjs-mf/utils';
import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    if (ctx?.pathname && !ctx?.pathname?.endsWith('_error')) {
      await revalidate().then((shouldUpdate) => {
        if (shouldUpdate) {
          console.log('Hot Module Replacement (HMR) activated', shouldUpdate);
        }
      });
    }

    const initialProps = await Document.getInitialProps(ctx);
    return initialProps;
  }

  render() {
    return (
      <Html>
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

Chunk Flushing

Chunk flushing attempts to "flush out" use chunks during SSR so that <script> tags can be sent to the browser.

_document
import Document, { Html, Head, Main, NextScript } from 'next/document';
import {
  revalidate,
  FlushedChunks,
  flushChunks,
} from '@module-federation/nextjs-mf/utils';

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    if (ctx.pathname) {
      if (!ctx.pathname.endsWith('_error')) {
        await revalidate().then((shouldUpdate) => {
          if (shouldUpdate) {
            console.log('should HMR', shouldUpdate);
          }
        });
      }
    }

    const initialProps = await Document.getInitialProps(ctx);

    const chunks = await flushChunks();

    return {
      ...initialProps,
      chunks,
    };
  }

  render() {
    return (
      <Html>
        <Head>
          <FlushedChunks chunks={this.props.chunks} />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

Step 4: Create and Expose

Now, create a component to expose from MFE2

Create Button Component

In MFE2, create a new file named Button.js in the src directory with the following content:

import React from 'react';

const Button = () => (
  <button>MFE2 Button</button>
);

export default Button;

Configure next MFE2

Update build config to expose a module

next.config.mjs
const nextConfig = {
  reactStrictMode: true,
  webpack(config, options) {
    config.plugins.push(
      new NextFederationPlugin({
        name: 'mfe2',
        filename: 'static/chunks/remoteEntry.js',
        exposes: {
          "./Button": './component/Button.js',
        },
        shared: {},
        extraOptions: {
          exposePages: true,
          enableImageLoaderFix: true,
          enableUrlLoaderFix: true,
        },
      })
    )
    return config
  }
};

Step 5: Consume Remote Module

Consume the exposed module from MFE2 in MFE1

Import the module

importing MFE2 in one of the pages of MFE1

index.js
import React from 'react';
import Button from 'mfe2/Button'; // federated import

const Index = () => {
  return (
    <div>
      <h1>MFE1</h1>
      <Button />
    </div>
  );
}

Add Server Lifecycle Method to Page

Next will also attempt to SSG pages that do not have some data lifecycle.

Ensure one is added to page files.

export const getServerSideProps = async () => {
  return {
    props: {}
  }
}
// or
Index.getInitialProps = async ()=> {
  return {}
}

export default Index;

Configure Next in MFE1

Update the remotes field accordingly

next.config.mjs
import { NextFederationPlugin } from '@module-federation/nextjs-mf';

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  webpack(config,options ){
    config.plugins.push(
      new NextFederationPlugin({
        name: 'mfe1',
        filename: 'static/chunks/remoteEntry.js',
        remotes: {
          mfe2: `http://localhost:3001/static/${options.isServer ? 'ssr' : 'chunks'}/remoteEntry.js`,
        },
        shared: {},
        extraOptions: {
          exposePages: true,
          enableImageLoaderFix: true,
          enableUrlLoaderFix: true,
        },
      })
    )
    return config
  }
};

export default nextConfig;