Next.js 15 Pages Router: useRouter Breaks When Declaring the Plugin
Describe the bug
I am using Next.js 15 with the pages directory. When I declare the NextFederationPlugin in next.config.mjs, the useRouter hook from next/router throws the following error:
Error:
NextRouter was not mounted.
Link: https://nextjs.org/docs/messages/next-router-not-mounted
However, when I remove the plugin, useRouter works as expected. Since my project is using the pages router, next/router should be functioning correctly.
Steps to Reproduce
- Set up a Next.js 15 project with the pages directory.
- Install
@module-federation/nextjs-mf. - Configure
next.config.mjsto useNextFederationPlugin. - Try to use
useRouterinside a component. - Observe the error message about
NextRouternot being mounted. - Remove the plugin and notice that
useRouterworks again.
Expected Behavior
useRoutershould work even when theNextFederationPluginis added tonext.config.mjs.
Actual Behavior
useRouterthrows an error whenNextFederationPluginis declared.
Environment
Package.json (dependencies)
{
"dependencies": {
"@module-federation/nextjs-mf": "^8.8.21",
"next": "15.2.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"webpack": "^5.98.0"
}
}
Next.js Config (next.config.mjs)
import { NextFederationPlugin } from '@module-federation/nextjs-mf';
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
webpack(config, options) {
config.plugins.push(
new NextFederationPlugin({
name: 'mfe1',
filename: 'static/chunks/remoteEntry.js',
remotes: {
mfe2: `http://localhost:3001/static/${options.isServer ? 'ssr' : 'chunks'}/remoteEntry.js`,
},
shared: {},
extraOptions: {
exposePages: true,
enableImageLoaderFix: true,
enableUrlLoaderFix: true,
},
})
);
return config;
},
};
export default nextConfig;
Additional Notes
- This issue occurs only when
NextFederationPluginis present. - Removing the plugin restores the expected behavior of
useRouter. - This might be a conflict between Module Federation and Next.js's internal routing system.
Would appreciate any insights on whether this is a bug or if there's a missing configuration. Thanks! 🚀
Reproduction
https://codesandbox.io/p/github/dani0f/nextjbug/main?import=true
Used Package Manager
npm
System Info
System:
OS: Linux 6.1 Ubuntu 20.04.6 LTS (Focal Fossa)
CPU: (2) x64 AMD EPYC
Memory: 3.16 GB / 4.14 GB
Container: Yes
Shell: 5.0.17 - /bin/bash
Binaries:
Node: 20.12.1 - /home/codespace/nvm/current/bin/node
Yarn: 1.22.19 - /usr/bin/yarn
npm: 10.5.0 - /home/codespace/nvm/current/bin/npm
pnpm: 8.15.6 - /home/codespace/nvm/current/bin/pnpm
Validations
- [x] Read the docs.
- [x] Read the common issues list.
- [x] Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
- [x] Make sure this is a Module federation issue and not a framework-specific issue.
- [x] The provided reproduction is a minimal reproducible example of the bug.
@dani0f I'm having the same problem, which at the time is preventing us from patching the last vulnerability. Please share whatever workaround you could find!
@dani0f Same issue I'm also facing
try sharing next/compat/router.
next/compat/router and see if it works? it looks like they might have changed something in v15
Changing to use next/compat/router didn't really worked for me as it changes the behavior too much, and most of the time returns null instead of the router unfortunately. Adding it to the share didn't change anything too.
The required file that does not seems to be not included as shared is next/dist/shared/lib/router-context.shared-runtime but I was unable to make it work, probably for my lack of understanding.
@ScriptedAlchemy would you have some suggestions on what else I could try?
They definitely change it it stopped working from 15.2.X
I'll probably have to contact vercel about this
I conducted an experiment comparing Next.js versions 14 and 15 in a Module Federation setup. The results show that the router only works correctly when passed as a prop to the federated module. Other approaches either result in errors or return null.
Experiment Results
The table below shows the behavior of the router inside the remote module under different host and remote version combinations.
| Scenario | next/router |
next/compat/router |
Prop |
|---|---|---|---|
| Host 14 with Remote 14 | Works | No error, but router is null |
Works |
| Host 14 with Remote 15 | Works | No error, but router is null |
Works |
| Host 15 with Remote 14 | Error | No error, but router is null |
Works |
| Host 15 with Remote 15 | Error | No error, but router is null |
Works |
Solution
The only way I found to use the router in a federated module is to:
- Import it from
next/router/compatin the host. - Pass it as a prop to the remote module.
- Use it inside the remote component via props.
I wonder if it's due to sharing issues. From what chunk is the next router fetched? The router should load from the host only. Is the router loading from more than the host port, or any other port? The runtime plugin checks for the next router and then forwards the reference to its own React from its internal scopes.
@ScriptedAlchemy
It does seems to be related to sharing.
Importing the next/router works on the host if I remove it from the list of shared dependencies like this:
but unfortunately I was unable to make sharing it using module federation. the props things should work but It is not feasible on my codebase
what if you share it as the next/compat/router?
shared: {
'next/compat/router': {
requiredVersion: false,
singleton: true,
import: undefined
}
}
causes the router returned by
import { useRouter } from 'next/compat/router';
to be null on the host app.
shared: {
'next/router': {
requiredVersion: false,
singleton: true,
import: 'next/compat/router'
}
}
also causes the router returned by
import { useRouter } from 'next/router';
to be null on the host app.
@ScriptedAlchemy It does seems to be related to sharing. Importing the
next/routerworks on the host if I remove it from the list of shared dependencies like this:
but unfortunately I was unable to make sharing it using module federation. the props things should work but It is not feasible on my codebase
This was the only thing that helped me fix the issue with the router on the host so i was able to use it there, but it still gives some issues on the loaded mfe.
Having the same issue.
The config from ciandt-crodrigues helped to resolve the issue on the host app (no errors, redirecting works ok).
@ScriptedAlchemy It does seems to be related to sharing. Importing the
next/routerworks on the host if I remove it from the list of shared dependencies like this:but unfortunately I was unable to make sharing it using module federation. the props things should work but It is not feasible on my codebase
This was the only thing that helped me fix the issue with the router on the host so i was able to use it there, but it still gives some issues on the loaded mfe.
Dont know if this belongs in this issue but using "next/head" also appears to have stopped working. For example setting browser titles with the <Head> component no longer sets the title.
Also removing "next/head" from the defaultShared dependencies, like in your example, seems to be working as a workaround for the time being.
@ScriptedAlchemy Sorry to bother, but just to be clear. At the moment, no one is working on this issue, and there is no plan to have someone working on this due to NextJS being in maintenance mode?
I've shared a workaround here, but unfortunately it does not work for me, tried my best to troubleshoot and help but I don't know how does module federation works with the shared dependencies and I find myself unable to proceed by myself. I would really love to help but the inner works on how this works does not seems clear to me just by looking at the code.
Support for Nextjs as a whole is actively being worked on right now. Sometimes we're a bit behind on issues. apologies.
Okay so theres some bug / backward compat i didnt factor in when i designed layered sharing - i need to test it further but 0.0.0-next-20250612091126 seems to make next.js 15 work without react hook error etc.
For next/router, the reason you get router not mounted or null is becasue router is strictly layer based, and default one in my share config is unlayered and i did not change share config in the canary.
but you can add this to your own config and seems to work at a glance
@ScriptedAlchemy, thanks for the reply!
I've got a different error with that:
"next": "^15.3.3" "@module-federation/nextjs-mf": "0.0.0-next-20250612091126" With and without the shared section for next/compat/router
Look like server issue, server and client have different layers.
Something like this may work.
layer: config.isServer ? 'pages-dir-node' : 'pages-dir-browser',
issuerLayer: config.isServer ? 'pages-dir-node' : 'pages-dir-browser'
I also pushed a canary of v9 which may be a little better. It has some of the share configs where the layer stuff is baked in, however not all.
you can try v9 canary 0.0.0-next-20250616235047 this seems to work
Also note: When you expose or share your own packages, if they depend on react - this causes additional issues. Because your shared module is not specifying a layer, it falls through or falls back to a different variant of react in the bundle. Basically it falls through to 'unlayered' react, while next itself actually uses 3 copies of react, 1 on each layer.
In this v9, i have updated federation core plugin where i adjust how these layered shared are dealt with.
I had chatgpt help detail the underlaying problem and mechanics a little, as its complex - but would be helpful to understand whats going on.
Next.js 15 Pages Router Layer Conflicts & Module Federation Sharing
Thanks for reporting this issue! This is a known problem that relates to how Module Federation handles layered shared modules in Next.js, particularly around React sharing between different execution contexts.
Root Cause: Layer Key Mismatches & Share Scope Resolution
The useRouter issue you're experiencing is a symptom of a deeper problem with how Module Federation's sharing system works with Next.js layers. Here's what's actually happening:
How Module Federation Sharing Works in Next.js (with Layers)
Overview
Module Federation's sharing system in Next.js is built around the concept of layered module resolution. This is especially important in Next.js 13+ (and v15), where different parts of the app (pages, app, server, client) may run in different "layers" and require different versions or instances of shared modules like React.
How Sharing is Configured
- Exposed modules are always unlayered. When you expose a module, it does not have a
layerorissuerLayerby default. - Shared modules are only layered by default if the user does not specify a different
layerfield. If you provide alayerin your shared config, that will be used; otherwise, the default layer is applied. - The
issuerLayeris used internally to match shared requests to shared modules. When a module is requested, the system tries to match the request'sissuerLayerto the shared module'slayer.
How Sharing is Matched
The core matching logic is in resolveMatchedConfigs.ts:
- Key Generation:
Each shared config is stored in a map using a composite key:- If
issuerLayeris set, the key is(<issuerLayer>)<request>, e.g.(pages-dir-browser)react - If
issuerLayeris not set, the key is just<request>, e.g.react
- If
- Three Maps:
resolved: For direct file paths (absolute or relative)unresolved: For module names (likereact,next/router)prefixed: For module prefixes (likenext/dist/shared/)
When a module is requested, the plugin tries to match the request using the composite key. If the request comes from a specific layer, it looks for a layered key. If not, it looks for an unlayered key.
Why Layering Matters
- Layered Sharing:
Next.js creates layered shared configs for core modules (like React, Next.js internals) so that each execution context (pages, app, server, RSC) can have its own isolated instance if needed. - Unlayered Sharing:
If you expose or share your own packages (like a design system, UI library, or utility) and do not specify alayer, those modules will be stored and matched as unlayered. This can cause them to fall through to a different instance of a shared dependency (like React) than what Next.js uses in its own layers.
Example: The Layered vs. Unlayered Key Problem
// Exposed module (always unlayered)
{
request: 'my-exposed-component',
shareKey: 'my-exposed-component',
layer: undefined,
issuerLayer: undefined // Always unlayered
}
// Shared module (layered by default unless user specifies otherwise)
{
request: 'react',
shareKey: 'react',
layer: 'pages-dir-browser',
issuerLayer: 'pages-dir-browser' // Used for matching
}
- If a federated module requests 'react' without an issuerLayer, it will look for the unlayered key ('react').
- If Next.js requests 'react' from a specific layer, it will look for the layered key ('(pages-dir-browser)react').
- If your shared module (like antd) requests 'react' and is not layered, it may get a different instance than the rest of the app, leading to context mismatches.
What to Watch Out For
- Exposed and shared modules without an
issuerLayerwill always match the unlayered key. This is the root cause of many context and version issues in Next.js 15 with Module Federation v8. - If you share your own packages (like antd, design systems, or utilities), be aware that their dependencies (like React) may fall through to the unlayered version unless you explicitly layer them.
- Incorrect or missing layer configuration can cause silent failures, context mismatches, and hard-to-debug bugs.
Background Context
- The v9 plugin update introduces better handling for these cases by allowing
issuerLayer: undefinedto fall back to the correct shared instance, but you must still be careful with your own shared module configurations. - The test repo at https://github.com/ScriptedAlchemy/next-apps-ejected demonstrates these concepts in practice.
This background should help you understand why sharing sometimes "just works" and sometimes fails in subtle ways, especially as you add more shared modules or use third-party libraries in a federated Next.js setup.
Also
How i currently got it to work, where pages router app exposes something from like the shop app, and it works in the home app. I set react and next router so that they share with layer, but additionally without a issuerLayer.
issuerLayer is 'what layer is the module requesting react in' - so we have to know if its a RSC component asking for react or a use client or ssr etc, 'layer' is how we will create out consume shared FallbackModule to match.
To maintain a singleton for anything where one hasnt set this on their own shares, or when i expose a module. I have specified a shared module with a layer, but with no issuerLayer - so exposed modules and shared modules and default share scope will essentially point back to pages-dir-browser as and pages-dir-node.
This will be problematic for app dir, because user will have to specify the layer or the next plugin must automatically take unlayered shares and split them into 3 layers under the hood. The problem is, if some share module contains server only or use client and then the rsc loader (who is applied to all modules with the rsc layer or issuerLayer), will throw the build upon parsing a forbidden module.
and exposed modules as mentioned dont even have a config convention - so setting layer or issuerLayer would require a major change. But what i experimented with is makeing a directory for rsc and pages, and then have a loader test those paths and just apply layer: 'rsc' - which is what /pages/ and /app/ are doing currently inside next.
TLDR: use follow this implementation or use the version in this repo to test it out. https://github.com/ScriptedAlchemy/next-apps-ejected Ill make edits and install new canary into it to test out v15 and v9
@ScriptedAlchemy
thanks for all the hard work! We managed to apply this in our project and seems to work with latest NextJS and React versions 😄 I was wondering, is there a plan in a short-term to release a more "stable" version rather than the canary one 0.0.0-next-20250616235047 as you mentioned before? I've read that one of the goals is to make the pages dir work as it was, and we were wondering if a new version is going to be released soon to make this update asap :) thanks in advance!
@ScriptedAlchemy
thanks for all the hard work! We managed to apply this in our project and it seems to work with the latest NextJS and React versions 😄 I was wondering, is there a plan in a short-term to release a more "stable" version rather than the canary one
0.0.0-next-20250616235047as you mentioned before? I've read that one of the goals is to make the pages dir work as it was, and we were wondering if a new version is going to be released soon to make this update asap :) thanks in advance!
Its being worked on. Follow my prs around app-router-share-filter and share-filter branches. Its a large update. I've been trying to get the tests to pass with both next 14 and next 15. Not easy since v15 is significantly different. On top of that, interoperability between 14 and 15 when sharing code is also unknown.
Once CI is green. I'll try to get some of this pushed to at least a beta or alpha tag on npm.
This is a very big change to both next plugin and module federation.
@ScriptedAlchemy Hi when can we expect to have a stable version for this issue. Canary version seems to be working but getting few css issues. Please try to release a stable version.
@ScriptedAlchemy Hi when can we expect to have a stable version for this issue. Canary version seems to be working but getting few css issues. Please try to release a stable version.
Contribute if you want it faster.
@ScriptedAlchemy
Thanks for sharing that repo link for testing out new MF! I was curious to test out the exposed-pages example since that is how we leverage MF (using the build plugin + loadRemote + Pages Router) and when trying to load that page I see this error when running pnpm dev
Not sure if it is expected or not?
This pattern is working well for us on Next 14 + MF v8, fwiw.