core icon indicating copy to clipboard operation
core copied to clipboard

Next.js App router and module-federation/nextjs-mf

Open dohyuns-kim opened this issue 2 years ago • 26 comments

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.

dohyuns-kim avatar Aug 06 '23 07:08 dohyuns-kim

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.

ElHawary-Ebutler avatar Aug 09 '23 09:08 ElHawary-Ebutler

Next.js App Router is not currently supported in nextjs-mf

ScriptedAlchemy avatar Aug 09 '23 23:08 ScriptedAlchemy

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.

dohyuns-kim avatar Aug 10 '23 08:08 dohyuns-kim

No app router support

ScriptedAlchemy I see. I'll have to find another way. Thanks!

dohyuns-kim avatar Aug 10 '23 08:08 dohyuns-kim

@ScriptedAlchemy When will the next release for module-federation/nextjs-mf be? Will it support App Router?

chenmeister avatar Aug 24 '23 00:08 chenmeister

@chenmeister we will likely implement it first in modernjs.dev, then see how to backport it to next. If it can be

ScriptedAlchemy avatar Aug 25 '23 00:08 ScriptedAlchemy

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

ScriptedAlchemy avatar Sep 30 '23 01:09 ScriptedAlchemy

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 ☹️

madmonkey08 avatar Oct 17 '23 22:10 madmonkey08

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

DannyBoris avatar Nov 12 '23 23:11 DannyBoris

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??

Mainak908 avatar Nov 19 '23 10:11 Mainak908

https://github.com/module-federation/universe/pull/2002

ScriptedAlchemy avatar Jan 21 '24 22:01 ScriptedAlchemy

@ScriptedAlchemy I watched your YouTube video on module federation with NextJs and SSR Link . Was it with page router and not app router?

subho951m avatar Mar 17 '24 10:03 subho951m

No app router support. Will never have app router support.

ScriptedAlchemy avatar Mar 18 '24 12:03 ScriptedAlchemy

No app router support. Will never have app router support.

Doesn't this contradict that statement? https://twitter.com/rauchg/status/1768656965109252517

Dannymx avatar Mar 23 '24 01:03 Dannymx

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

ScriptedAlchemy avatar Mar 23 '24 13:03 ScriptedAlchemy

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 avatar Apr 07 '24 22:04 rohit20001221

@rohit20001221 Can you share an example of your solution please

michael-motornyi avatar May 10 '24 23:05 michael-motornyi

Use module federation/runtime loadRemote and init. Don't inject container on your own like this.

ScriptedAlchemy avatar May 11 '24 06:05 ScriptedAlchemy

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

rohit20001221 avatar May 12 '24 22:05 rohit20001221

You don't need get it you have lib sync available.

ScriptedAlchemy avatar May 14 '24 19:05 ScriptedAlchemy

Is it possible to have next-mf working with app to router (client components)?


michael-motornyi avatar May 14 '24 23:05 michael-motornyi

@ScriptedAlchemy : Can you please tell me when we are targetting to add app router support in nextjs-mf?

sumit12690 avatar May 27 '24 12:05 sumit12690

@ScriptedAlchemy : Can you please tell me when we are targetting to add app router support in nextjs-mf?

he said never above

stephen-dahl avatar Jun 07 '24 17:06 stephen-dahl

@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?

rdenman avatar Jun 10 '24 16:06 rdenman

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()],
});

Antignote avatar Jul 09 '24 13:07 Antignote

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...

mdjfs avatar Aug 28 '24 16:08 mdjfs

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

ScriptedAlchemy avatar Oct 25 '24 16:10 ScriptedAlchemy

I can't set Module Federation in Next js app router

wubeabera123 avatar Mar 20 '25 12:03 wubeabera123

Check pinned issue. Next support is in maintenance mode.

ScriptedAlchemy avatar Mar 20 '25 23:03 ScriptedAlchemy

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.

areeshalam avatar Apr 04 '25 14:04 areeshalam