next/dynamic in a module imported by both routers gets the App Router module id in react-loadable-manifest.json — pages-router hydration fails with React #418 (webpack)
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 142k
- Forks
- 32.4k
- Avg merge
- 2d 14h
- Merged PRs (30d)
- 351
Description
Link to the code that reproduces this issue
https://github.com/korhan/next-loadable-layer-repro
To Reproduce
bun install(ornpm install)bun run build— runsnext build --webpackbun run check— compares the idreact-loadable-manifest.jsonrecords forcomponents/Shared.jsx -> ./Lazywith the id the pages client bundle asks forbun run start, open http://localhost:3400/ with the console open
The project is four components: pages/index.jsx and pages/other.jsx both import components/Shared.jsx; app/app-route/page.jsx imports the same Shared through a 'use client' wrapper; Shared calls dynamic(() => import('./Lazy')).
Current vs. Expected behavior
Current. check prints:
manifest key : components/Shared.jsx -> ./Lazy
manifest id : 7017
dynamic() ids in client JS: 7017 (app/app-route/page-*.js) | 7678 (pages/index-*.js, pages/other-*.js)
BUG: manifest id 7017 is the App Router copy's; the pages client asks for 7678, never preloads Lazy, and hydrates its fallback (React #418)
On /, __NEXT_DATA__.dynamicIds is [7017], the server HTML contains <p id="lazy"> (Lazy was server-rendered), and the browser logs
Uncaught Error: Minified React error #418; visit https://react.dev/errors/418?args[]=HTML&args[]=
React then regenerates the tree client-side. Production builds only; next dev is clean.
Expected. The manifest entry for a pages-router dynamic() carries the pages-layer module id (7678 here), dynamicIds lists it, Loadable.preloadReady resolves Lazy before hydration, and the page hydrates without errors — as it does when Shared is imported by the pages router alone.
Cause. ReactLoadablePlugin (packages/next/src/build/webpack/plugins/react-loadable-plugin.ts) keys the manifest by `${importer} -> ${request}` with no notion of webpack layer. A module imported by both routers exists twice in the client compilation — once in pages-dir-browser, once in app-pages-browser — with two module ids. Both copies are walked under the same key and whichever is walked last writes the entry's id. When the App Router copy is last, the pages server emits an id the pages client has no initializer for. Which copy is last is an ordering accident of compilation.modules: with a single pages importer webpack concatenates the pages copy and it happens to win; with two pages importers (any real site) the App Router copy wins. In development id is the key string itself, so both layers agree and the bug is invisible.
Only the pages router consumes this manifest (app-render threads reactLoadableManifest through the work store but never reads entries), so the App Router entries are never used — they only overwrite the ones that are. Skipping import() blocks whose importer has layer === 'app-pages-browser' fixes it; the repo carries that as react-loadable-plugin.patch, and with it applied check prints OK: manifest id 7678 is the pages-layer id, dynamicIds is [7678], and the page hydrates clean. Happy to open a PR.
Provide environment information
Operating System:
Platform: darwin
Arch: arm64
Version: Darwin Kernel Version 25.6.0
Available memory (MB): 32768
Available CPU cores: 10
Binaries:
Node: 22.13.1
npm: 11.1.0
Relevant Packages:
next: 16.4.0-canary.21 // Latest available version is detected (16.4.0-canary.21).
eslint-config-next: N/A
react: 19.2.3
react-dom: 19.2.3
typescript: N/A
Next.js Config:
output: N/A
Reproduced on 16.3.2 (the version pinned in the repo) and on 16.4.0-canary.21.
Which area(s) are affected? (Select all that apply)
Lazy Loading, Pages Router, Webpack
Which stage(s) are affected? (Select all that apply)
next build (local), next start (local), Vercel (Deployed), Other (Deployed)
Additional context
Hit while incrementally adopting the App Router in a large pages-router site: shared header, layout and widgets are imported by both trees, and 48 of the 318 dynamic() call sites in our client bundle ended up with the App Router id. The visible symptom was #418 on the home page only, because that was the route whose server render happened to include one of the affected components; every pages route rendering one of them is affected.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in packages/next/src/build/webpack/plugins/react-loadable-plugin.ts and reproduce the issue with the linked next-loadable-layer-repro project using bun run build and bun run check. Trace how modules from the pages-dir-browser and app-pages-browser layers populate react-loadable-manifest.json, then add regression coverage showing that pages-router dynamic imports receive the pages-layer id and hydrate without the reported mismatch.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, next.js, react, webpack
- Domain
- build-system, frontend, web-dev
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 75/100