feat: encode interception context in App Router payload IDs and caches#753
Conversation
commit: |
There was a problem hiding this comment.
💡 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".
| case "navigate": | ||
| return getCurrentInterceptionContext(); |
There was a problem hiding this comment.
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 👍 / 👎.
49d0a21 to
64d7696
Compare
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.
64d7696 to
2fa57c8
Compare
…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.
|
/bigbonk review |
There was a problem hiding this comment.
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
-
HMR handler omits interception context — The
rsc:updatehandler (line 1056-1088 inapp-browser-entry.ts) doesn't sendX-Vinext-Interception-Contexton 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. -
History state initialization effect dependencies — The
useLayoutEffectat line 500-513 has[treeState.interceptionContext, treeState.renderId]in its dependency array, but theif (treeState.renderId !== 0) return;guard means only therenderId === 0invocation ever executes the body. TheinterceptionContextdep is technically unnecessary but the exhaustive-deps rule would require it, so this is fine — just slightly misleading to read. -
Redirect history state — On redirect (line 936-939),
createHistoryStateWithInterceptionContext(null, requestInterceptionContext)discards any existing history state keys. Since only__vinext_interceptionContextis stored today, this is safe. Worth watching if more keys are added to history state in the future. -
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.
|
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:
|
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
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/42modal navigations stop aliasing direct/photos/42page 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
__interceptionContextwhen an intercepted render is active, and route/page IDs are encoded with that context instead of keying only by pathnameapp-browser-entryandapp-browser-statenow carry committed interception context alongsideelements,routeId, androotLayoutTreePath, and write it into history state so traversal can recover the original source route/photos/42.rscfrom/feedand/gallerydo 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 onenext/linkprefetch and App Router navigations sendX-Vinext-Interception-Contextwhen the current render is inside an intercepted flow, and the server reads that header when building intercepted payloadsgallerysource route plus targeted integration/E2E coverage for direct-vs-intercepted cache separation, repeated intercepted navigations, refresh behavior, and interception-aware prefetch entriesWhat 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.tsvp 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-routergit commitpre-commit hook (vp check --fix)