fix: share-link hydration dead on dev hard loads (useSearchParams dehydration)#159
Merged
Conversation
Under next dev (Next 16 / React 19), a hard navigation to a URL with query params left the Suspense boundary around every useSearchParams() consumer permanently dehydrated: the prerendered HTML stayed visible but React never attached (no fibers, no effects), so share-link hydration silently never ran on /encounters, /maps, /noncombat, and /noncombat/player. Client-side transitions and the production static build were unaffected. In a fully static export, query params are a client-only concern: the one-shot hydration effects now read window.location.search directly (which never suspends), and the player page derives its projection from a mount-time query state with a popstate listener. The now-unneeded Suspense wrappers are removed. A source-scan regression test forbids reintroducing the hook in page components.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Root cause
Under
next dev(Next 16 / React 19), a hard navigation to any URL with query params left the Suspense boundary around auseSearchParams()consumer permanently dehydrated: the prerendered HTML stayed visible but React never attached — no fibers on the DOM, no effects ever ran — so share-link hydration silently did nothing on/encounters,/maps,/noncombat, and/noncombat/player. Client-side transitions and the production static build were unaffected, which is why it went unnoticed.Diagnosed by instrumentation, not guesswork: a module-level marker proved the right bundle executes; a fiber check proved the subtree never hydrates on hard loads; the same effect logs twice (StrictMode) and hydrates fine via client-side nav — the one-shot
didInitguard (the original suspect) was actually correct.Fix
In a fully static export, query params are a client-only concern:
new URLSearchParams(window.location.search)directly — effects only run client-side and this never suspends./noncombat/player(which read params during render) now derives its projection from mount-time query state with apopstatelistener; the prerendered "Preparing the handout…" state matches the first client render, so hydration is mismatch-free.page.tsxforbids reintroducing theuseSearchParamsimport/call (red before the fix: 4 failures; green after).Verification
npm run typecheckclean ·npx vitest run46 files, 691/691 ·npm run lint0 errors ·npm run buildstatic export succeeds.Also includes a one-line
autoPortaddition to.claude/launch.json(dev convenience; port 3000 was occupied locally).