Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
dohyuns-kim opened this issue Aug 6, 2023 · 27 comments
Closed

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

dohyuns-kim opened this issue Aug 6, 2023 · 27 comments
Labels

Comments

@dohyuns-kim
Copy link

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/next@13.4.12
@babel+core@7.22.9_react-dom@18.2.0_react@18.2.0/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.

@ElHawary-Ebutler
Copy link

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.

@ScriptedAlchemy
Copy link
Member

ScriptedAlchemy commented Aug 9, 2023

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

@dohyuns-kim
Copy link
Author

dohyuns-kim commented Aug 10, 2023

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
Copy link
Author

dohyuns-kim commented Aug 10, 2023

No app router support

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

@chenmeister
Copy link

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

@ScriptedAlchemy
Copy link
Member

ScriptedAlchemy commented Aug 25, 2023

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

@ScriptedAlchemy
Copy link
Member

Not RSC, but there has been some passive progress made for next.js in general.

#1268 resolves or should resolve any and all module consumption related problems.
Thanks to #1224 - I now have absolute control

Specifically this: #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

@madmonkey08
Copy link

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

@DannyBoris
Copy link

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

@Mainak908
Copy link

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

Do u find any solution yet??

@ScriptedAlchemy ScriptedAlchemy changed the title Nextjs 13.4.12 App router and module-federation/nextjs-mf 7.0.6 Next.js App router and module-federation/nextjs-mf Dec 31, 2023
@ScriptedAlchemy
Copy link
Member

#2002

@subho951m
Copy link

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

@ScriptedAlchemy
Copy link
Member

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

@Dannymx
Copy link

Dannymx commented Mar 23, 2024

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

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

@ScriptedAlchemy
Copy link
Member

ScriptedAlchemy commented Mar 23, 2024

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

@rohit20001221
Copy link

rohit20001221 commented Apr 7, 2024

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

@michael-motornyi
Copy link

@rohit20001221 Can you share an example of your solution please

@ScriptedAlchemy
Copy link
Member

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

@rohit20001221
Copy link

rohit20001221 commented May 12, 2024

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

@ScriptedAlchemy
Copy link
Member

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

@michael-motornyi
Copy link

michael-motornyi commented May 14, 2024 via email

@sumit12690
Copy link

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

@stephen-dahl
Copy link

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

he said never above

@rdenman
Copy link

rdenman commented Jun 10, 2024

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

@Antignote
Copy link

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/@module-federation+sdk@0.2.5/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/@module-federation+sdk@0.2.5/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()],
});

@mdjfs
Copy link

mdjfs commented Aug 28, 2024

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

@ScriptedAlchemy
Copy link
Member

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 ScriptedAlchemy closed this as not planned Won't fix, can't repro, duplicate, stale Nov 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests