muhsen97
muhsen97

Reputation: 87

HMR not Working With Nextjs Module Federation Plugin

I have two micro-frontend apps, main and checkout. For my MFE setup I'm using Next.js along with @module-federation/nextjs-mf. The main app consumes components from the checkout app. The issue is that when I change any exposed component, HMR does not work.

When I refresh the page I get hydration errors because the server still has the previous version, and I have to rerun the app to get the latest changes from the server.

What could be the issue here?

Here are the configs for both apps in next.config.js:

// for the main app
webpack: (config, { isServer }) => {
    const location = isServer
      ? '_next/static/ssr/remoteEntry.js'
      : '_next/static/chunks/remoteEntry.js';

    config.plugins.push(
      new NextFederationPlugin({
        name: 'main',
        filename: 'static/chunks/remoteEntry.js',
        exposes: {},
        remotes: {
            checkout: `checkout@http://localhost:3001/${location}`
        },
        shared: {},
        extraOptions: {
          enableImageLoaderFix: true,
          enableUrlLoaderFix: true,
        },
      })
    );
    return config;
  },
// for the checkout app
webpack: (config) => {
    config.plugins.push(
      new NextFederationPlugin({
        name: 'checkout',
        filename: 'static/chunks/remoteEntry.js',
        exposes: {
          './checkout': './pages/checkout.tsx',
        },
        shared: {},
        extraOptions: {
          enableImageLoaderFix: true,
          enableUrlLoaderFix: true,
          exposePages: true,
        },
      })
    );
    return config;
  },

Note: I'm using nx for my monorepo setup.

Upvotes: 1

Views: 94

Answers (1)

Zeros-N-Ones
Zeros-N-Ones

Reputation: 1082

This is a common issue with Module Federation and HMR, especially in Next.js. There are some potential solutions:

First, try adding automaticAsyncBoundary: true to your NextFederationPlugin configuration in both apps:

// In both apps
new NextFederationPlugin({
  // ... other config
  extraOptions: {
    automaticAsyncBoundary: true,
    enableImageLoaderFix: true,
    enableUrlLoaderFix: true,
  },
})

If that doesn't work, you can try implementing a development-only reload mechanism:

// In your main app where you import the remote component
import dynamic from 'next/dynamic'

const CheckoutComponent = dynamic(() => import('checkout/checkout'), {
  ssr: false,
  loading: () => <div>Loading...</div>
})

// Add this in development mode
if (process.env.NODE_ENV === 'development') {
  const ws = new WebSocket('ws://localhost:3001')
  ws.onmessage = () => {
    window.location.reload()
  }
}

Another approach is to modify the webpack configuration to better handle HMR:

// In the main app
webpack: (config, { isServer }) => {
  const location = isServer
    ? '_next/static/ssr/remoteEntry.js'
    : '_next/static/chunks/remoteEntry.js';

  config.plugins.push(
    new NextFederationPlugin({
      name: 'main',
      filename: 'static/chunks/remoteEntry.js',
      remotes: {
        checkout: `promise new Promise(resolve => {
          const remoteUrl = 'http://localhost:3001/${location}'
          const script = document.createElement('script')
          script.src = remoteUrl
          script.onload = () => {
            // Initialize the remote
            const proxy = {
              get: (request) => window.checkout.get(request),
              init: (arg) => {
                try {
                  return window.checkout.init(arg)
                } catch(e) {
                  console.log('remote container already initialized')
                }
              }
            }
            resolve(proxy)
          }
          document.head.appendChild(script)
        })`
      },
      // ... rest of your config
    })
  );
  return config;
}

For the hydration errors, you can try disabling SSR for the remote components:

// In your next.config.js of the main app
module.exports = {
  experimental: {
    esmExternals: false,
    externalDir: true,
  },
}

And in your component:

const CheckoutComponent = dynamic(() => import('checkout/checkout'), {
  ssr: false,
  loading: () => <div>Loading...</div>
})

Since you're using Nx, make sure you have the correct configuration in your nx.json:

{
  "tasksRunnerOptions": {
    "default": {
      "runner": "nx/tasks-runners/default",
      "options": {
        "cacheableOperations": ["build", "test", "lint", "e2e"],
        "parallel": true
      }
    }
  }
}

I suggest trying these solutions in this order:

  • First try the automaticAsyncBoundary option as it's the simplest
  • If that doesn't work, implement the development-only reload mechanism
  • If you're still having issues, try the more complex webpack configuration
  • If hydration errors persist, disable SSR for the remote components

Upvotes: 1

Related Questions