I am trying to implement a importing a react app, built using CRACO, into a nextjs app. I have tried multiple different implementations, but cannot get the import to work as a module import. I have pulled in https://www.npmjs.com/package/@module-federation/nextjs-mf and https://www.npmjs.com/package/@module-federation/utilities. Trying all example implementations. I am able to import my component dynamically using either next/dynamic
or importRemote
, however loading dynamically prevents me from using the full suite of functionality.
All of this aside, the biggest issue is that when I import dynamically the next/router back functionality breaks.
I pull in the component and it loads. Underneath the module federated component I am rendering a link, upon click the link the nextjs app correctly updates the browser history and updates the DOM, directing me to a new page. If i click the back button, or add a button on my new page that mimics the back functionality, I can see the browser history update but the DOM does not update.
I feel like this has something to do with how I am importing but I cannot figure out how to resolve this issue.
I am running an nextJS host which is pulling in a react component via module federation.
Next v12.0.7 React v17.0.2
I am stuck on these dependencies due to downstream internal projects which have not updated to latest react.
Remote: The exposed remote function builds a div and renders the root of the micro-frontend header:
initialize.tsx
import * as ReactDOM from 'react-dom';
import './index.scss';
import TopNav from './TopNav';
const initialize = () => {
var headerContainer = document.createElement('div');
headerContainer.id = 'header';
document.body.prepend(headerContainer);
const root = document.getElementById('header');
ReactDOM.render(<TopNav />, root);
return null;
};
export default initialize;
modulefederation.config.js
const deps = require('./package.json').dependencies;
const federationConfig = require('./federation.config.json');
module.exports = {
...federationConfig,
name: 'test',
filename: 'remoteEntry.js',
shared: {
...deps,
react: {
singleton: true,
requiredVersion: false,
},
'react-dom': {
singleton: true,
requiredVersion: false,
},
},
};
Host: I have tried multiple ways of using module federation within Nextjs. From dynamic remotes to delegate remotes. These implementations all throw errors when I try to use a normal import.
different ways I tried to get remotes
~Delegates~
remote-delegate.js:
import { importDelegatedModule } from '@module-federation/utilities';
module.exports = new Promise((resolve, reject) => {
console.log('Delegate being called for', __resourceQuery);
const currentRequest = new URLSearchParams(__resourceQuery).get('remote');
const [global, url] = currentRequest.split('@');
importDelegatedModule({ global, url, })
.then((container) => { resolve(container); })
.catch((err) => reject(err));
});
next.config.js:
const remotes = {
header: 'promise "./remote-delegate.js?remote=consoleui_header@https://localhost:8080/remoteEntry.js"',
};
webpack: (config, options) => {
return {
plugins: [
...config?.plugins,
new NextFederationPlugin({
name: 'host',
remotes,
filename: `static/${location}/remoteEntry.js`,
}),
],
};
},
};
~Dynamic Promise~
next.config.js:
const remotes = {
header: 'promise "./remote-delegate.js?remote=consoleui_header@https://localhost:8080/remoteEntry.js"',
};
webpack: (config, options) => {
return {
plugins: [
...config?.plugins,
new NextFederationPlugin({
name: 'host',
remotes: {
header: `promise new Promise(resolve => {
const remoteUrlWithVersion = 'https://localhost:8080/remoteEntry.js'
const script = document.createElement('script')
script.src = remoteUrlWithVersion
script.onload = () => {
// the injected script has loaded and is available on window
// we can now resolve this Promise
const proxy = {
get: (request) => window.app1.get(request),
init: (arg) => {
try {
return window.app1.init(arg)
} catch(e) {
console.log('remote container already initialized')
}
}
}
resolve(proxy)
}
// inject this script with the src set to the versioned remoteEntry.js
document.head.appendChild(script);
})
`,
},
filename: `static/${location}/remoteEntry.js`,
}),
],
};
},
};
Neither of these implementations worked as I'd like. I couldn't import like a normal module and had to fall back to dynamic importing.
~Dynamic Imports~
This is the best implementation that allows me to access the most from my module, however I still can only access it dynamically and this is where the back button shows up. However I have tested the back button with both Dynamic Promise
and Delegates
and ran into the same issue.
next.config.js
webpack: (config, options) => {
return {
plugins: [
...config?.plugins,
new NextFederationPlugin({
name: 'host',
remotes: {
header: `consoleui_header@https://localhost:8080/remoteEntry.js`,
},
filename: `static/${location}/remoteEntry.js`,
}),
],
};
},
_app.tsx
import dynamic from "next/dynamic";
const Initialize = dynamic(() => import ('header/initialize').then(mod => mod.default()),{
ssr: false,
})
function MyApp({Component, pageProps }: any {
return <>
<Initialize />
{!isLoading &&
<Component {...pageProps} />
}
</>
}
/test/index.tsx
import Link from 'next/link'
import React from 'react'
const list = () => (
<Link href={"test/detail?id=123"} > list page</Link>
)
export default list
/test/detail.tsx
import Link from 'next/link'
import React from 'react'
const detail = () => (
<Link href={"test/detail?id=123"} > details page</Link>
)
export default detail
In this example if I remove <Initialize />
from the return of my app, the back button works as expected.