Reputation: 87
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
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:
automaticAsyncBoundary
option as it's the simplestUpvotes: 1