Skip to content

feat: encode interception context in App Router payload IDs and caches#753

Open
NathanDrake2406 wants to merge 11 commits intocloudflare:mainfrom
NathanDrake2406:feat/layout-persistence-pr-4-interception-encoding
Open

feat: encode interception context in App Router payload IDs and caches#753
NathanDrake2406 wants to merge 11 commits intocloudflare:mainfrom
NathanDrake2406:feat/layout-persistence-pr-4-interception-encoding

Conversation

@NathanDrake2406
Copy link
Copy Markdown
Contributor

Summary

Part of #726 (PR 4). This adds the interception-context identity that PR 2c intentionally deferred. Intercepted App Router navigations now carry their source route through the payload IDs, prefetch cache, visited-response cache, and history state, so /feed -> /photos/42 modal navigations stop aliasing direct /photos/42 page loads.

The practical effect is that intercepted and direct renders of the same visible pathname no longer stomp each other. Repeated feed/gallery modal navigations reuse the right cached payload, back/forward restores the right source context, and the ordinary visited-response cache still works for non-intercepted navigations.

What changed

  • Payload metadata and IDs — App Router payloads now include __interceptionContext when an intercepted render is active, and route/page IDs are encoded with that context instead of keying only by pathname
  • Browser router stateapp-browser-entry and app-browser-state now carry committed interception context alongside elements, routeId, and rootLayoutTreePath, and write it into history state so traversal can recover the original source route
  • Prefetch and visited-response caches — prefetch keys and visited-response keys are now interception-aware, so /photos/42.rsc from /feed and /gallery do not alias each other. Direct-route payloads still omit interception metadata, so visited-response storage preserves the request-side cache partition when the payload does not provide one
  • Client requestsnext/link prefetch and App Router navigations send X-Vinext-Interception-Context when the current render is inside an intercepted flow, and the server reads that header when building intercepted payloads
  • Fixtures and tests — adds a gallery source route plus targeted integration/E2E coverage for direct-vs-intercepted cache separation, repeated intercepted navigations, refresh behavior, and interception-aware prefetch entries

What this does NOT do

Server action / refresh parity for intercepted routes is still deferred. This PR intentionally leaves action re-renders on the direct-route path and keeps the scope to payload encoding + browser-side cache/history identity.

Test plan

  • vp test run tests/app-elements.test.ts tests/app-browser-entry.test.ts tests/prefetch-cache.test.ts
  • vp test run tests/app-router.test.ts -t "renders intercepted photo modal on RSC navigation from feed"
  • vp run test:e2e -- tests/e2e/app-router/advanced.spec.ts --project app-router
  • git commit pre-commit hook (vp check --fix)

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 2, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@753

commit: 1a9a4dd

@NathanDrake2406 NathanDrake2406 marked this pull request as ready for review April 2, 2026 12:59
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e1f1be6cbc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +320 to +321
case "navigate":
return getCurrentInterceptionContext();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reuse stored interception context for soft navigations

When navigationKind === "navigate", this always derives the context from window.location.pathname, which drops the original source context after the first intercepted transition. For example, after opening /photos/42 as a modal from /feed, subsequent in-app navigations from that modal send X-Vinext-Interception-Context: /photos/42 instead of /feed; the server then resolves the direct route and the modal flow breaks into a full-page render. The code already stores interception context in history state during commit, so using pathname here causes a real regression for chained intercepted navigations.

Useful? React with 👍 / 👎.

@NathanDrake2406 NathanDrake2406 marked this pull request as draft April 2, 2026 13:10
@NathanDrake2406 NathanDrake2406 force-pushed the feat/layout-persistence-pr-4-interception-encoding branch 2 times, most recently from 49d0a21 to 64d7696 Compare April 10, 2026 15:30
Prove the flat keyed map architecture works end-to-end:
- Layout state persists across sibling navigation (counter survives)
- Template remounts on segment boundary change, persists within segment
- Error boundary clears on navigate-away-and-back
- Back/forward preserves layout state through history
- Parallel slots persist on soft nav, show default.tsx on hard nav

Zero production code changes — test fixtures and Playwright specs only.
@NathanDrake2406 NathanDrake2406 force-pushed the feat/layout-persistence-pr-4-interception-encoding branch from 64d7696 to 2fa57c8 Compare April 10, 2026 15:35
…ements signature

PR 3 added mounted-slot tests using createResolvedElements(routeId, root, extraEntries).
PR 4 inserted interceptionContext as the 3rd param, shifting extraEntries to 4th.
The object was being swallowed as interceptionContext, causing getMountedSlotIds to return [].
Also add missing renderId to the interception context pending-commit test.
@NathanDrake2406 NathanDrake2406 marked this pull request as ready for review April 10, 2026 15:42
@james-elicx
Copy link
Copy Markdown
Collaborator

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Interception context encoding

This is a well-structured PR that systematically threads interception context through the entire payload/cache/history lifecycle. The cache partitioning strategy (NUL separator, history state persistence, request header) is sound, and the test coverage is thorough — the E2E tests for chained navigations, cache separation, and refresh behavior cover the important scenarios.

A few observations and one suggestion follow.

Codex bot comment is incorrect

I verified the Codex bot's concern about getCurrentInterceptionContext on chained navigations (line 326 in app-browser-entry.ts). The history state commit happens via the pre-paint effect during React layout commit, which completes before the user can interact with the newly rendered modal. So when the user clicks a link inside the modal, getCurrentInterceptionContext() reads the stored /feed context from history state — not window.location.pathname. The E2E test at advanced.spec.ts:96-109 confirms this.

Observations

  1. HMR handler omits interception context — The rsc:update handler (line 1056-1088 in app-browser-entry.ts) doesn't send X-Vinext-Interception-Context on its re-fetch. If a developer is viewing an intercepted modal and HMR fires, the server builds a direct-route payload, replacing the modal with the full page. The server action handler explicitly documents this gap (line 742-743). Consider adding a similar comment in the HMR handler so the omission reads as intentional rather than overlooked.

  2. History state initialization effect dependencies — The useLayoutEffect at line 500-513 has [treeState.interceptionContext, treeState.renderId] in its dependency array, but the if (treeState.renderId !== 0) return; guard means only the renderId === 0 invocation ever executes the body. The interceptionContext dep is technically unnecessary but the exhaustive-deps rule would require it, so this is fine — just slightly misleading to read.

  3. Redirect history state — On redirect (line 936-939), createHistoryStateWithInterceptionContext(null, requestInterceptionContext) discards any existing history state keys. Since only __vinext_interceptionContext is stored today, this is safe. Worth watching if more keys are added to history state in the future.

  4. NUL separator — Good choice using "\0" for cache key separation. It cannot appear in URL pathnames, making collisions impossible.

Suggestion

Add a comment in the HMR handler to document the intentional omission of interception context, matching the server action handler's documentation pattern. Something like:

// Intentionally omit interception context for HMR re-renders.
// Preserving intercepted modal state across HMR belongs to PR 5.

Overall this is solid work. The cache partitioning design is clean, the test matrix covers the key scenarios (direct vs intercepted, repeated navigations, refresh, prefetch separation), and the deferred scope (server actions/refresh) is clearly documented.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk bot commented Apr 10, 2026

Review posted on PR #753. Here's a summary of my findings:

The PR is solid. It cleanly threads interception context through payloads, cache keys, history state, and request headers so that intercepted and direct renders of the same pathname don't stomp each other.

Key findings:

  1. Codex bot comment is wrong — I traced the chained navigation flow and confirmed that getCurrentInterceptionContext() correctly reads the stored /feed context from history state (written during the pre-paint commit effect), not from window.location.pathname. The E2E test confirms this works.

  2. One actionable suggestion — The HMR rsc:update handler omits X-Vinext-Interception-Context (modal breaks on HMR), similar to the server action handler. But the server action handler has an explicit comment documenting this as intentional; the HMR handler doesn't. Suggested adding a matching comment.

  3. Minor observations — dependency array includes interceptionContext in a guard-protected effect (fine, just slightly misleading), redirect path discards existing history state keys (safe today, watch for future), and the NUL separator choice is a good one.

github run

Add a comment to the HMR rsc:update handler noting that interception
context is intentionally omitted, matching the existing documentation
pattern in the server action handler.

https://claude.ai/code/session_01EnvJuSwsZS9n862q7Veamo
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.

3 participants