vercel / vercel/next.js

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)

Open
#98,384 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Lazy Loading Pages Router Webpack
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
  1. bun install (or npm install)
  2. bun run build — runs next build --webpack
  3. bun run check — compares the id react-loadable-manifest.json records for components/Shared.jsx -> ./Lazy with the id the pages client bundle asks for
  4. bun 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.