Skip to content

Use structural React keys during server rendering - #98944

Open
gnoff wants to merge 4 commits into
canaryfrom
codex/structural-server-keys
Open

gnoff wants to merge 4 commits into
canaryfrom
codex/structural-server-keys

Conversation

@gnoff

@gnoff gnoff commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Use structural React keys during server rendering so an HTML prerender can resume without rewriting React's postponed state when route params become known. Browser keys and router-cache identity are unchanged.

For example, /products/[slug] previously prerendered layout boundaries with a key containing an opaque slug placeholder. Resuming /products/shoe produced a different key, so Next.js substituted shoe into the serialized React state before handing it back to React. Both server passes now use slug|d. The browser still uses slug|shoe|d, preserving template remounts and back/forward state behavior.

The server only renders the active entry at each layout-router position, so it does not need parameter values to distinguish cached browser entries. React keys are not embedded in the HTML; hydration derives its concrete keys from the request's route state.

  • Use segment name/type for server layout keys and structural route paths for the server head key. Server keys always omit search params. The existing browser-module variant mechanism selects the concrete key implementation for browser bundles in both Webpack and Turbopack.
  • Remove placeholder substitution and the request-params argument from postponed-state parsing. Persist only fallback param names for render staging; React's postponed state no longer needs to be searched or rewritten.
  • Simplify the remaining opaque fallback map to param name → placeholder. Its dynamic-param type slot is unused after removing interpolation. Parameter wrappers still control suspension; this does not make unknown params available to application code.
  • Test server and browser keys directly, including catch-all/intercepted segments and search params. Update the action-only resume fixture's single assertion on the resume-metadata format so its three cases reach their behavioral checks for action execution, revalidation, and notFound() with names-only metadata.

This refactor is independently landable against canary and is now the base of the parameter-matching stack: #98944#98891#98892#98893#97393. It supersedes #98890. It retains internal opaque placeholders but removes their role in matching React's postponed state. No new integration fixtures are needed.

Verification

Latest cleanup:

  • 126 focused unit tests and 4 snapshots pass in both default and Cache Components/Webpack configurations.
  • 34 production Webpack tests pass across fallback-shells, partial-fallback-shell-upgrade, action-only-fallback-resume-data-cache, and resuming-head-runtime-search-param; 4 existing tests remain skipped. The three action-only resume cases now reach and pass their behavioral assertions.

Earlier commits also passed existing production and dev Webpack coverage for back/forward state, template remounts, base-path params, nested/parallel/intercepted routes, form-state hydration, and standalone/edge output.

A reviewer verified this head under Turbopack using a native binary built from the same commit: a broader unit selection passed (144 tests, the focused set above plus the remaining router-reducer suites), the packages/next type check was clean, and 33 production e2e tests passed across fallback-shells, partial-fallback-shell-upgrade, and action-only-fallback-resume-data-cache.

React needs stable keys between HTML prerender and resume, but unknown route params currently put opaque placeholders in those keys. Next.js then rewrites the serialized postponed state with concrete request values.

Use segment name/type for server layout keys and structural paths for the server head key. For /products/[slug], both server passes use slug|d while the browser still uses slug|shoe|d. Keep browser remount behavior and router cache identity unchanged.

Remove postponed-state placeholder substitution and its request-params argument. Retain fallback-param metadata for render staging, update existing parser expectations, and document the distinction.

Validated with 87 existing Webpack integration tests, 63 unit tests, TypeScript, lint, and a full build. Local Turbopack runs stop before rendering because the installed native binding does not provide the project result shape expected by this canary checkout.
@github-actions

github-actions Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Tests Passed

Commit: 6878e71

Use a single createSegmentKey import for layouts and the head. The default implementation uses structural keys on the server, while a .browser.ts variant re-exports the existing concrete router key function. For /products/[slug], the server retains slug|d across prerender and resume, while the browser uses slug|shoe|d so navigation still resets the appropriate subtree.

Compute the layout state key once and reuse it for template, Activity, and dev boundaries. Remove inline server/browser key selection and the separate per-entry React key. Preserve query-only state reuse and browser head keys, and regenerate both Webpack and Turbopack browser-variant alias lists.

Verified with 63 existing unit tests and 43 existing Webpack integration tests across dev and production, including fallback-shell resume/hydration, head search params, back/forward state, template remounting, and standalone/edge output. Full JS build, TypeScript, ESLint, Prettier, and generated Rust formatting pass. Turbopack integration remains unverified locally because the available native binding does not match this checkout; it also needs the regenerated alias list compiled into it.
Structural server keys no longer need opaque parameter placeholders or their
types to resume React's HTML. Persist just the unknown parameter names for
render staging, and parse them into a set. For /products/[slug], the metadata
is now ["slug"] instead of [["slug", ["%%drp:slug:...%%", "d"]]].

Use name sets in the request store while retaining the separate placeholder
map used by dev prerender validation. Preserve explicit-empty metadata and
the platform's metadata-free 4:nullnull state: the former defers no params,
while the latter still falls back to the request's staging metadata. Remove
the unused serialized-entry type and the stale interpolation comment.

Update serialization coverage for HTML and data-only states, scalar and
catch-all params, cache compression, and empty/full prelude detection.
Verified 71 unit tests and 39 Webpack integration tests across production
and dev, including shell upgrades, revalidation, hydration, dev validation,
head resumption, and standalone/edge output. Four pre-existing integration
tests remain skipped. Build, TypeScript, ESLint, and Prettier pass. Local
Turbopack integration remains unverified because the available native
binding does not match this checkout.
Add direct coverage for the server and browser segment-key implementations.
For example, server keys stay slug|d when an opaque fallback becomes a real
slug, while browser keys include the concrete value. Cover catch-all and
intercepted segments, distinct names/types, and the different search-param
behavior. Delegate static server segments to the existing router cache-key
helper with search parameters always excluded.

Remove the unused dynamic-param type slot from opaque fallback maps. Resume
metadata now persists only param names, so these maps only need to associate
each name with its placeholder. Rename the old searchValue local and clarify
that parameter wrappers, not the placeholders themselves, control suspension.

Update the action-only fallback fixture to recognize names-only postponed
metadata. Its old tuple-format assertion caused all three local cases to fail
before exercising action execution, revalidation, and notFound handling.

Verification:
- 126 focused unit tests and 4 snapshots pass in both default and Cache
  Components/Webpack configurations.
- 34 production Webpack tests pass across fallback shells, shell upgrades,
  action-only fallback resumes, and runtime-search-param head resumes;
  4 existing tests remain skipped.
- Full workspace build, repository type check, and changed-file lint pass.
- The Turbopack action-resume run stops before application rendering because
  the installed native binding does not match this checkout's project result
  shape. CI needs to verify it with a matching native build.
gnoff added a commit that referenced this pull request Sep 19, 2026
Move the matching API onto the prerequisite chain rooted at #98944:
structural server keys, build-time generator context, cached fallback-root
reads, and adapter fallback query names. Preserve the API's existing history
with an integration merge and retain its independently removable diagnostic
logging commit.

The three prerequisite commits replay without patch changes. The API's
added and removed lines also remain identical to its previous diff against
the old prerequisite branch. Resolve overlap with canary's config additions
and closed-parameter hint plumbing without changing matching semantics.

Keep #98944's name-to-placeholder fallback map and structural server keys.
For example, a catch-all shell and its resumed render use the same parts|c
server key, so the old singleton-array placeholder conversion from #98890
is no longer part of the stack.

Verification: full workspace build and repository type check pass; all 222
focused static-paths, segment-key, fallback-param, and postponed-state unit
tests pass with four snapshots.

Focused Webpack integration coverage passes: 43 production tests across the
API, cached generators, existing gSP behavior, fallback root caching, and
adapter query output; 26 development tests cover the API and existing static
shell validation, with nine snapshots. Existing mode-specific skips remain
unchanged. Turbopack integration still needs the matching CI native build.
@gnoff
gnoff marked this pull request as ready for review September 19, 2026 21:59
@gnoff
gnoff added this pull request to stack #98946 September 19, 2026 22:03
@gnoff
gnoff removed this pull request from stack #98946 September 19, 2026 23:55
@gnoff
gnoff added this pull request to stack #98951 September 19, 2026 23:55
@gnoff
gnoff removed this pull request from stack #98951 September 20, 2026 05:46
@gnoff
gnoff added this pull request to stack #98956 September 20, 2026 05:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant