Pages Router: ssr:false dynamic imports mismatch when hydration retries after the import resolves
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/zjsng-trav/next-dynamic-hydration-repro/tree/c79e709
To Reproduce
git clone https://github.com/zjsng-trav/next-dynamic-hydration-repro.git
cd next-dynamic-hydration-repro
git checkout c79e709
npm ci
npm install next@16.4.0-canary.22
npm run build -- --webpack
npm start
- Open
http://localhost:3000/?mode=retryin a fresh document. The page server-renders a room section and excludes a header usingdynamic(..., { ssr: false }). - During hydration, a sibling throws a stable promise. The fixture releases it 50 ms after the header import resolves, so React retries hydration with the module already loaded.
- Check the browser console and the on-page evidence panel. React reports a hydration mismatch;
originalCapturedistrue, butoriginalRetainedandoriginalConnectedbecomefalse. - Open
/?mode=retry-no-boundaryin a fresh document. Removing the explicit Suspense boundary still mismatches, with root-level recovery. - Compare
/?mode=plain,/?mode=plain-no-boundary, and/?mode=ssr-retry. These controls retain the original server-rendered nodes without hydration errors.
For development mode, use npm run dev -- --webpack. Close the error overlay to inspect the recovered page. Use fresh document loads rather than hot reloads between cases.
Current vs. Expected behavior
Current: Once the import resolves during a suspended hydration attempt, the Pages Router loadable component reads the loaded state when React retries. Its output now includes a header absent from the server HTML. React discards the affected server-rendered content and recovers with client rendering. The recovered buttons work; this report is about the mismatch and discarded SSR content, not persistent duplication.
Expected: Hydration should continue using the loading state rendered by noSSR, then display the loaded component through a normal client update after hydration completes.
The Pages Router loader currently passes subscription.getCurrentValue as both the client and hydration snapshots in useSyncExternalStore. A local change that uses a stable server loading snapshot only for ssr: false removes the mismatch while leaving the import and suspension sequence unchanged.
Provide environment information
Operating System:
Platform: darwin
Arch: arm64
Version: Darwin Kernel Version 25.6.0
Binaries:
Node: 22.22.2
npm: 11.19.0
Yarn: 1.22.22
pnpm: 10.21.0
Relevant Packages:
next: 16.4.0-canary.22
react: 18.3.1
react-dom: 18.3.1
eslint-config-next: N/A
typescript: N/A
Next.js Config:
output: N/A
Browser:
Chromium: 152.0.7977.83
Bundler:
Webpack
Which area(s) are affected? (Select all that apply)
Pages Router, Lazy Loading, React, Webpack
Which stage(s) are affected? (Select all that apply)
next dev and production builds served by next start.
Additional context
Checked the latest published canary, 16.4.0-canary.22, on 9 September 2026. Also reproduced on stable 14.2.35, 15.5.25, and 16.3.4 with React 18.3.1, and on canary with React 19.0.0.
For the canary production A/B comparison with React 18.3.1, each of the five modes was loaded three times per variant. Stock canary mismatched in both retry modes on all three runs; the loader-only patch retained the original nodes with no hydration errors in every run. All controls passed in both variants. The React 19.0.0 comparison gave the same result.
The proposed regression fixture replaces the timing delay with an explicit browser-controlled promise: wait until both suspension and import completion are observed, then release hydration. Its retry cases fail on the original-DOM identity assertion before the patch and pass afterward. The no-suspension and SSR-enabled controls pass before and after.
The suspension is deliberate and synthetic. This demonstrates a reproducible ordering problem; it does not establish how frequently applications encounter that ordering. Turbopack and deployed Vercel builds have not been tested.
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 with the linked next-dynamic-hydration-repro and the Pages Router loadable loader, focusing on the useSyncExternalStore snapshots mentioned in the report. Run the listed build and start commands, then compare the retry and control modes; done means the retry cases retain the original server-rendered nodes without hydration mismatch errors.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, next.js, react, webpack
- Domain
- frontend, web-dev
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 72/100