core
core copied to clipboard
Next.js App router and module-federation/nextjs-mf
Hi!
I'm currently working on applying the newly created Nextjs Project as a remote package by applying Module Federation. However, it is a tougher situation than expected.
If I insert NextFederationPlugin under in next.config.js, an error occurs in the browser console. As follows
if (!options.isServer) {
config.plugins.push(
new NextFederationPlugin({
name: 'dmpro',
remotes: {},
filename: 'static/chunks/remoteEntry.js',
exposes: {
},
shared: {},
extraOptions:{
}
}),
);
}
app-index.js:171 Uncaught TypeError: (0 , react.use) is not a function at ServerRoot (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected]@[email protected][email protected][email protected]/node_modules/next/dist/client/app-index.js:171:33) at renderWithHooks (react-dom.development.js:16305:18) at mountIndeterminateComponent (react-dom.development.js:20069:13) at beginWork (react-dom.development.js:21582:16) at HTMLUnknownElement.callCallback (react-dom.development.js:4164:14) at Object.invokeGuardedCallbackDev (react-dom.development.js:4213:16) at invokeGuardedCallback (react-dom.development.js:4277:31) at beginWork$1 (react-dom.development.js:27446:7) at performUnitOfWork (react-dom.development.js:26552:12) at workLoopConcurrent (react-dom.development.js:26538:5) at renderRootConcurrent (react-dom.development.js:26500:7) at performConcurrentWorkOnRoot (react-dom.development.js:25733:38) at workLoop (scheduler.development.js:266:34) at flushWork (scheduler.development.js:239:14) at MessagePort.performWorkUntilDeadline (scheduler.development.js:533:21)
This is the position on the stack.
app-index.js
function ServerRoot(param) { _s(); let {cacheKey} = param; _react.default.useEffect(()=>{ rscCache.delete(cacheKey); } ); const response = useInitialServerResponse(cacheKey); const root = (0, _react.use)(response); <==== error return root; }
If I remove the Next Federation Plugin, the app works fine.
My development environment is as follows.
"dependencies": { "@apollo/client": "^3.7.17", "@module-federation/nextjs-mf": "7.0.6", "@react-google-maps/api": "^2.19.2", "@tanstack/react-query": "^4.32.0", "@tanstack/react-query-devtools": "^4.32.0", "axios": "^1.4.0", "dayjs": "^1.11.9", "firebase": "^10.1.0", "graphql": "^16.7.1", "graphql-ws": "^5.14.0", "next": "^13.4.12", "next-pwa": "^5.6.0", "react": "18.2.0", "react-dom": "18.2.0", "styled-components": "^6.0.5", "usehooks-ts": "^2.9.1" },
Can you help me?
Thanks, Dohyun.
App router is not supported yet. I have the same issue. Please check here https://github.com/module-federation/universe/issues/961
My personal go to for now since i need the app router, is using the good old webpack. still working on that.
Next.js App Router is not currently supported in nextjs-mf
App router is not supported yet. I have the same issue. Please check here #961
My personal go to for now since i need the app router, is using the good old webpack. still working on that.
ElHawary-Ebutler Thank you for your kind reply. I wonder if I can keep the app router until my service is completed, but since you are saying that you are implementing it with Module Federation Plugin, I should try that method.
@ScriptedAlchemy When will the next release for module-federation/nextjs-mf be? Will it support App Router?
@chenmeister we will likely implement it first in modernjs.dev, then see how to backport it to next. If it can be
Not RSC, but there has been some passive progress made for next.js in general.
https://github.com/module-federation/universe/pull/1268 resolves or should resolve any and all module consumption related problems. Thanks to https://github.com/module-federation/universe/pull/1224 - I now have absolute control
Specifically this: https://github.com/module-federation/universe/pull/1433
Should resolve "call of undefined" and "eager consumption" errors, particularly eager consumption, which has been the root cause of most of the Problems with Next.js - import() is no longer required for module federation to work
App router is not supported yet. I have the same issue. Please check here https://github.com/module-federation/universe/issues/961
My personal go to for now since i need the app router, is using the good old webpack. still working on that.
Was it challenging to implement microfrontends with Webpack? I also need to incorporate the app router into my application while implementing microfrontends ☹️
App router is not supported yet. I have the same issue. Please check here #961 My personal go to for now since i need the app router, is using the good old webpack. still working on that.
Was it challenging to implement microfrontends with Webpack? I also need to incorporate the app router into my application while implementing microfrontends ☹️
@ElHawary-Ebutler im curious too
App router is not supported yet. I have the same issue. Please check here https://github.com/module-federation/universe/issues/961
My personal go to for now since i need the app router, is using the good old webpack. still working on that.
Was it challenging to implement microfrontends with Webpack? I also need to incorporate the app router into my application while implementing microfrontends ☹️
Do u find any solution yet??
https://github.com/module-federation/universe/pull/2002
@ScriptedAlchemy I watched your YouTube video on module federation with NextJs and SSR Link . Was it with page router and not app router?
No app router support. Will never have app router support.
No app router support. Will never have app router support.
Doesn't this contradict that statement? https://twitter.com/rauchg/status/1768656965109252517
That's a proprietary implementation they've done. Won't be compatible. It's not module federation. But it is a welcome step in the right direction!
Theirs will only work for server components. No use client etc
https://x.com/scriptedalchemy/status/1769330325149339667?s=46
Hi everyone
i have found a way to use dynamic module federation with app router
useDynamicScript.ts
import React from "react";
const useDynamicScript = (args: { url: string }) => {
const [ready, setReady] = React.useState(false);
const [failed, setFailed] = React.useState(false);
React.useEffect(() => {
if (!args.url) {
return;
}
const element = document.createElement("script");
element.src = args.url;
element.type = "text/javascript";
element.async = true;
setReady(false);
setFailed(false);
element.onload = () => {
console.log(`Dynamic Script Loaded: ${args.url}`);
setReady(true);
};
element.onerror = () => {
console.error(`Dynamic Script Error: ${args.url}`);
setReady(false);
setFailed(true);
};
document.head.appendChild(element);
return () => {
console.log(`Dynamic Script Removed: ${args.url}`);
document.head.removeChild(element);
};
}, [args.url]);
return {
ready,
failed,
};
};
export default useDynamicScript;
RemoteComponent.tsx
"use client";
import React, { Suspense } from "react";
import useDynamicScript from "../../hooks/mfe.hooks";
globalThis.React = React;
async function loadComponent(scope: string, module: any) {
// @ts-ignore
const container = global[scope];
// @ts-ignore
await container.init(
// LOADING REACT
Object.assign(
{
react: {
"18.2.0": {
get: () => Promise.resolve(() => require("react")),
loaded: true,
},
},
},
// @ts-ignore
global.__webpack_require__ ? global.__webpack_require__.o : {}
)
);
// @ts-ignore
const factory = await global[scope].get(module);
const Module = factory();
return Module;
}
function RemoteComponent(props: {
module: string;
url: string;
scope: string;
children: any;
}) {
const { children } = props;
const { ready, failed } = useDynamicScript({
url: props.module && props.url,
});
if (!props.module) {
return <h2>Not system specified</h2>;
}
if (!ready) {
return <h2>Loading dynamic script: {props.url}</h2>;
}
if (failed) {
return <h2>Failed to load dynamic script: {props.url}</h2>;
}
if (!global) return null;
const Component = React.lazy(async () => {
const m = await loadComponent(props.scope, props.module);
return m;
});
return (
<Suspense fallback="Loading Module">
<Component>{children}</Component>
</Suspense>
);
}
export default RemoteComponent;
using RemoteComponent i was able to load a federated ReduxStoreProvider into the layout.tsx file in app router
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { Suspense } from "react";
import RemoteComponent from "@/lib/components/RemoteComponent";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<RemoteComponent
url="http://localhost:3000/remoteEntry.js"
module="./Container"
scope="remote_app"
>
{children}
</RemoteComponent>
</body>
</html>
);
}
initially this thing failed but after adding
globalThis.React = React
but i am not sure why setting globalThis.React made the dynamic module federation work with app router 😅
and one more change i have made so that the above snippet works is being specific about version of react, i have change react version from
^18 --> 18.2.0 in both remote and container
i am not a module federation expert so i am open for improvements in the above snippets
i create the above snippets taking reference from this video https://www.youtube.com/watch?v=d58QLA2bnug
@rohit20001221 Can you share an example of your solution please
Use module federation/runtime loadRemote and init. Don't inject container on your own like this.
hi @ScriptedAlchemy
is this the correct way to use federation runtime
first i am creating a federation instance using init
import { init } from "@module-federation/enhanced/runtime";
import React from "react";
import ReactDOM from "react-dom";
export const federation = init({
name: "next_app",
remotes: [],
shared: {
react: {
scope: "default",
lib: () => React,
get: () => () => require("react"),
shareConfig: {
singleton: true,
requiredVersion: "^18",
},
},
"react-dom": {
scope: "default",
lib: () => ReactDOM,
get: () => () => require("react-dom"),
shareConfig: {
singleton: true,
requiredVersion: "^18",
},
},
},
});
now in the remote component i am using the federation instance to load the remote component
this will render the remote component
"use client";
import { FC, Suspense, lazy, useEffect, useState } from "react";
import { federation } from "../utils/federation";
const RemoteComponentRuntime: FC<RemoteContainerRuntimeProps> = ({
url,
scope,
module,
}) => {
const [isClient, setIsClient] = useState(false);
useEffect(() => {
if (typeof window !== undefined && !isClient) setIsClient(true);
}, [isClient]);
if (!isClient) return null;
const Component = lazy(async () => {
if (!federation.moduleCache.has(scope)) {
try {
federation.registerRemotes(
[
{
entry: url,
name: scope,
alias: scope,
shareScope: "default",
},
],
{ force: true }
);
} catch (e) {
return { default: () => <div>{(e as Error).message}</div> };
}
}
const m = (await federation.loadRemote(`${scope}/${module}`)) as {
default: any;
};
return m;
});
return (
<Suspense>
<Component />
</Suspense>
);
};
export default RemoteComponentRuntime;
type RemoteContainerRuntimeProps = {
url: string;
scope: string;
module: string;
};
this hook will load the remote module at runtime
"use client";
import { useCallback, useEffect, useState } from "react";
import { federation } from "../utils/federation";
export const useRemoteModule = (props: {
url: string;
module: string;
scope: string;
}) => {
const { module, scope, url } = props;
const [isClient, setIsClient] = useState(false);
const [isReady, setIsReady] = useState(false);
const [isError, setIsError] = useState(false);
const [mod, setMod] = useState<any>();
const errorFallback = useCallback(() => {
setIsReady(true);
setIsError(true);
}, []);
useEffect(() => {
if (typeof window !== "undefined" && !isClient) setIsClient(true);
}, [isClient]);
useEffect(() => {
if (!isClient) return;
if (!federation.moduleCache.has(scope)) {
try {
federation.registerRemotes(
[
{
entry: url,
name: scope,
shareScope: "default",
},
],
{ force: true }
);
} catch {
return errorFallback();
}
}
(async () => {
try {
const m = await federation.loadRemote(`${scope}/${module}`);
setIsReady(true);
setMod(m);
} catch {
return errorFallback();
}
})();
}, [isClient]);
return {
isReady,
isError,
mod,
};
};
currently everything is client side rendering only if it is possible todo ssr with module federation runtime library then we dont need to worry about next.js supporting module federation or not we can use this low level api todo runtime sharing and for building the remoteEntry.js we can use some side car pattern with a mono repo
here is a example repo i have create https://github.com/rohit20001221/next-14-federated-sidecar/tree/feature/module-federation-runtime
You don't need get it you have lib sync available.
Is it possible to have next-mf working with app to router (client components)?
@ScriptedAlchemy : Can you please tell me when we are targetting to add app router support in nextjs-mf?
@ScriptedAlchemy : Can you please tell me when we are targetting to add app router support in nextjs-mf?
he said never above
@ScriptedAlchemy : Can you please tell me when we are targetting to add app router support in nextjs-mf?
he said never above
The issue was recently reopened though, and things may have changed with Module Federation v2 + the work he's been doing in the modernjs ecosystem. Should we keep our fingers crossed @ScriptedAlchemy or is full module federation SSR support for the app router still dead in the water?
I managed to get module federation runtime to work with nextjs app router for CSR but I'm facing issue with "self" not beeing assigned on the server side.
The shell app is nextjs 14 using app router and the remote frontend is build using rsbuild.
Any ideas on possible solutions?
The error is:
2024-07-09T13:16:09.872Z - error: [console] ⨯ ../node_modules/.pnpm/@[email protected]/node_modules/@module-federation/sdk/dist/index.cjs.js (650:15) @ handleScriptFetch
2024-07-09T13:16:09.874Z - error: [console] ⨯ Internal error: Error: Script execution error: ReferenceError: self is not defined
at handleScriptFetch (../../../node_modules/.pnpm/@[email protected]/node_modules/@module-federation/sdk/dist/index.cjs.js:635:16)
Loading of remote module in nextjs
'use client';
import RemoteContent from '@/@types/mfe/RemoteContent';
import { init, loadRemote } from '@module-federation/enhanced/runtime';
import { lazy, Suspense } from 'react';
import React from 'react';
import nodeRuntimePlugin from '@module-federation/node/runtimePlugin';
const isServer = typeof window === 'undefined';
init({
name: 'shell',
remotes: [
{
name: 'remote',
entry: `http://localhost:9000${isServer ? '/server' : ''}/remoteEntry.js`,
},
],
shared: {
react: {
version: '18.2.0',
scope: 'default',
lib: () => React,
shareConfig: {
singleton: true,
requiredVersion: '^18.2.0',
},
},
},
plugins: [nodeRuntimePlugin()],
});
const LoadedRemoteComponent = lazy(() => {
return loadRemote<{ default: typeof RemoteContent }>('remote/RemoteComponent', {
from: 'runtime',
}) as Promise<{ default: typeof RemoteContent }>;
});
export default function Sportinfo() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LoadedRemoteComponent />
</Suspense>
);
}
rsbuild config
import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';
import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
export default defineConfig({
output: {
targets: ['web', 'node'],
externals: {
bufferutil: 'webpack-node-externals',
'utf-8-validate': 'webpack-node-externals',
},
},
server: {
port: 9000,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
'Access-Control-Allow-Headers': '*',
},
},
dev: {
assetPrefix: 'http://localhost:9000/',
},
tools: {
rspack: (config, { appendPlugins, isServer }) => {
config.output!.uniqueName = 'remote';
if (isServer) {
appendPlugins([
new ModuleFederationPlugin({
remoteType: 'script',
runtimePlugins: [require.resolve('@module-federation/node/runtimePlugin')],
name: 'remote',
filename: 'remoteEntry.js',
library: { type: 'module' },
remotes: {},
exposes: {
'./RemoteComponent': './src/Component.tsx',
},
shared: {
react: {
singleton: true,
eager: true,
requiredVersion: '^18.2.0',
},
'react-dom': {
singleton: true,
eager: true,
requiredVersion: '^18.2.0',
},
},
}),
]);
} else {
appendPlugins([
new ModuleFederationPlugin({
name: 'remote',
filename: 'remoteEntry.js',
exposes: {
'./RemoteComponent': './src/Component.tsx',
},
shared: {
react: {
singleton: true,
eager: true,
requiredVersion: '^18.2.0',
},
'react-dom': {
singleton: true,
eager: true,
requiredVersion: '^18.2.0',
},
},
}),
]);
}
},
},
plugins: [pluginReact()],
});
I also was looking for the app router support, but sad, would be amazing if we have SSR support right now also have App Router support...
I'm going to close this issue out.
It seems more and more likely that we as the core team will stop supporting next.js
Next 15 will likely be the last version we maintain active support for.
If anyone in the community wants to take over the maintenance and development of the next integration, you can contact me about it.
There's better solutions for MFE than next.js
TanStack, nuxt, remix, modernjs, or anything built with vite.
If you really need next.js then you should module your organization the way vercel commands.
Maybe consider using zones from next
I can't set Module Federation in Next js app router
Check pinned issue. Next support is in maintenance mode.
App router is not supported yet. I have the same issue. Please check here #961
My personal go to for now since i need the app router, is using the good old webpack. still working on that.
But you can't use Module federation of Webpack with next, which script would you run? Next dev or webpack one? I tried and either the module federation would work or else next js would work like routing, ssr, etc.
It is somehow very hard to get the microfrontend work with next dev and idk how to use webpack and next dev together.