Search code examples
next.jsmicro-frontendwebpack-module-federation

Module Federation or Micro Frontend is not working in Next js version 12.2.0


I followed below steps to achieve module federation in next js.

  1. Create a component Insurance_Detail.tsx in repository cm-insurance-web inside src/node/components folder, which will be exposed. Below is the next.config.js file.
const assetPrefix = '/jobs-assets';
const nextConfig = {
  assetPrefix,
  env: {
    assetPrefix
  },
  experimental: {
    images: {
      unoptimized: true
    }
  },
  reactStrictMode: true,
  webpack5: true,
  srcDir: 'src/node/',
  //distDir: 'build',
  webpack: (config, options) => { // webpack configurations
    config.plugins.push(
        new options.webpack.container.ModuleFederationPlugin({
          name:"InsuranceA",
          filename: "static/chunks/pages/cm_insurance_web.js", // remote file name which will used later
          remoteType: "var",
          exposes: { // expose all component here.
            **"./InsuranceDetail": "./components/Insurance_Details.tsx"**
          },
          shared: [
            {
              react: {
                eager: true,
                singleton: true,
                requiredVersion: false,
              }
            },
            {
              "react-dom": {
                eager: true,
                singleton: true,
                requiredVersion: false,
              }
            },
          ]
        })
    )
      config.cache = false;
    config.output.publicPath = 'http://localhost:3000/_next/';
    return config
  }
}

module.exports = nextConfig
  1. When we build repo cm-insurance-web using command npm run build, we can see javascript file is being created inside src/node/.next/static/chunks/pages/cm_insurance_web.js. This project repo is running on 3000 port on localhost.

  2. Now need to consume this javascript in other repository let say cm-job-board-web. Let's create consumer app. Below is the next.config.js file for it

/** @type {import('next').NextConfig} */
const assetPrefix = '/jobs-assets';
const path = require('path');
const nextConfig = {
  assetPrefix,
  env: {
    assetPrefix
  },
  basePath: '/search-jobs',
  experimental: {
    images: {
      unoptimized: true
    }
  },
  reactStrictMode: true,
  srcDir: 'src/node/',
  webpack: (config, options) => {
    config.plugins.push(
        new options.webpack.container.ModuleFederationPlugin({
          name:"jobboardWeb",
          filename: "static/chunks/cm_job_board_web.js",
          remoteType: "var",
          remotes: {
              InsuranceA: JSON.stringify('InsuranceA@http://localhost:3000/jobs-assets/_next/static/chunks/pages/cm_insurance_web.js')
          },exposes: {},
            shared: [
                {
                    react: {
                        eager: true,
                        singleton: true,
                        requiredVersion: false,
                    }
                },
                {
                    "react-dom": {
                        eager: true,
                        singleton: true,
                        requiredVersion: false,
                    }
                },
            ]
        })
    )
      config.cache = false;
    return config
  },
  webpack5: true
}

module.exports = nextConfig
  1. add script tag in _app.tsx file of consumer app as follows:
import { AppProps } from "next/app";
import "bootstrap/dist/css/bootstrap.css";
import "../styles/globals.scss";
import Layout from "../components/layout";
import { persistor, store } from "../store/store";
import { Provider } from "react-redux";
import { PersistGate } from "redux-persist/integration/react";
import Authentication from "../config/auth.gaurd";
import Head from "next/head";
import React from "react";
import Script from "next/script";

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <Layout>
        <>
            <Script src="http://localhost:3000/jobs-assets/_next/static/chunks/pages/cm_insurance_web.js" />
            <Head>
                <link rel="preconnect" href="https://fonts.googleapis.com"/>
                <link rel="preconnect" href="https://fonts.gstatic.com"/>
                <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@100;200;300;400;500;600;700&display=swap"/>
                <link rel="shortcut icon" href="/favicon2.ico"/>
                <title>Jobboard Search</title>
            </Head>
            <Script id="gtm-script" strategy="afterInteractive">
                {`(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
                    new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
                    j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
                    'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
                    })(window,document,'script','dataLayer','GTM-TKJH8RR');`
                }
            </Script>
            <Provider store={store}>
                <PersistGate loading={null} persistor={persistor}>
                    <Authentication>
                        <noscript dangerouslySetInnerHTML={{ __html:
                            `<iframe src="https://www.googletagmanager.com/ns.html?id=GTM-TKJH8RR"
                            height="0" width="0" style="display:none;visibility:hidden"></iframe>
                            `}}>
                        </noscript>
                        <Component {...pageProps} />
                    </Authentication>
                </PersistGate>
            </Provider>
        </>
    </Layout>
  );
}

export default MyApp;
  1. Let’s import that module into the index.tsx file and use it.
import {NextPage} from "next";
import React, {lazy, Suspense, useState} from "react";
import dynamic from 'next/dynamic'


const InsuranceDetail2 = dynamic(() => import(('InsuranceA/InsuranceDetail')), {
    ssr: false
}) as NextPage;

const Insurance: NextPage = ({}: any) => {

    return (

            <InsuranceDetail2 />

    )
}


export default Insurance

After doing above steps I was able to see that remote js file is being loaded in browser's network tab but remote component is not getting rendered and getting blank page.

Please find attached screenshot

Please let me know if I am missing anything here. I took the reference from below links.

  1. https://blog.logrocket.com/micro-frontend-react-next-js/
  2. https://blog.logrocket.com/building-micro-frontends-webpacks-module-federation/
  3. https://dev.to/omher/building-react-app-with-module-federation-and-nextjsreact-1pkh

Solution

  • Installing nextjs-mf ⚠️ Attention: for the application to work with Module Federation features you need to have access to the https://app.privjs.com/package?pkg=@module-federation/nextjs-mf[[nextjs-ssr^] plugin which currently requires a paid license!

    To install the tool, we need to login to [PrivJs}(https://privjs.com/^) using npm, to do so, run the following command:

    npm login --registry https://r.privjs.com

    Once this is done a file containing your credentials will be saved in ~/.npmrc. Now you can install nextjs-mf using the command below:

    npm install @module-federation/nextjs-mf --registry https://r.privjs.com

    so module federation is paid module in next js and with the help of paid module, I am able to achieve it.

    1. next.config.js of insurance module.
    /** @type {import('next').NextConfig} */
    const NextFederationPlugin = require('@module-federation/nextjs-mf');
    const assetPrefix = '/jobs-assets';
    const nextConfig = {
      assetPrefix,
      env: {
        assetPrefix
      },
      reactStrictMode: true,
      webpack5: true,
      srcDir: 'src/node/',
      //distDir: 'build',
      webpack: (config, options) => { // webpack configurations
          if (!options.isServer) {
              config.plugins.push(
                  new NextFederationPlugin({
                      name: "insurancea",
                      filename: "static/chunks/pages/cm_insurance_web.js", // remote file name which will used later
                      exposes: { // expose all component here.
                          "./insurancedetail": "./components/Insurance_Details.tsx"
                      },
                      shared:
                          {
                              react: {
                                  singleton: true,
                                  requiredVersion: false,
                              }
                          }
                  }),
              );
          }
          return config
      }
    };
    
    module.exports = nextConfig
    
    
    1. add dependency of module federation in package.json.
    "dependencies": {
    
     "@module-federation/nextjs-mf": "^5.9.2",
    }
    
    1. add import in _app.tsx file of same insurance module.
    import '@module-federation/nextjs-mf/src/include-defaults';
    

    That's it for expose component

    1. Now update it for remote component (consumer app - cm-job-board-web) in next.config.js
    /** @type {import('next').NextConfig} */
    const NextFederationPlugin = require('@module-federation/nextjs-mf');
    const assetPrefix = '/jobs-assets';
    const path = require('path');
    const nextConfig = {
      assetPrefix,
      env: {
        assetPrefix
      },
      basePath: '/search-jobs',
      reactStrictMode: true,
      srcDir: 'src/node/',
      webpack: (config, options) => {
          if (!options.isServer) {
              config.plugins.push(
                  new NextFederationPlugin({
                      name: "jobboardWeb",
                      filename: "static/chunks/cm_job_board_web.js",
                      remotes: {
                          //  cm_insurance_web: options.isServer ? 'http://localhost:3000/jobs-assets/_next/static/chunks/cm_insurance_web.js' : 'fe1'
                          insurancea: 'insurancea@http://localhost:3000/jobs-assets/_next/static/chunks/pages/cm_insurance_web.js'
                      }, exposes: {},
                      shared: {}
                  }),
              );
          }
        return config
      },
      webpack5: true
    };
    
    module.exports = nextConfig
    
    
    1. add dependency of module federation in package.json of consumer app.
    "dependencies": {
    
     "@module-federation/nextjs-mf": "^5.9.2",
    }
    
    1. add import in _app.tsx file of consumer app.
    import '@module-federation/nextjs-mf/src/include-defaults';
    
    1. finally import that module into the index.tsx file and use it in consumer app.
    import { Suspense } from 'react'
    import React from 'react'
    import dynamic from 'next/dynamic'
    
    const DynamicComponent4 = dynamic(
        () => import('insurancea/insurancedetail'),
        { loading: () => <p>Loading caused by client page transition ...</p>, ssr: false }
    )
    
    
    export default function Insurance() {
        return (
            <div>
    
                    <DynamicComponent4 />
    
            </div>
        )
    }
    

    That's it.