Summary
On soft navigation between two URLs that match the same shared dynamic layout but with different params (/projects/A → back → /projects/B), the client router reuses the committed subtree from /projects/A without fetching the RSC payload for /projects/B. The URL and <title> update to B, but everything rendered from the layout's server props (data fetched with A's param) remains A's. A layout-level key={param} does not help — the reused subtree is never re-rendered with fresh server output, so no new key is ever applied.
Repro
App-router app with a shared dynamic layout that fetches per-param data:
app/
dashboard/page.tsx // links to /projects/A and /projects/B
projects/[pid]/layout.tsx // async layout: fetches project by pid, renders client shell with it as props
projects/[pid]/page.tsx
Steps (client-side navigation throughout, e.g. <Link> clicks + browser back):
- Load
/dashboard.
- Click a link to
/projects/A → renders A correctly.
- Browser back to
/dashboard.
- Click a link to
/projects/B.
Expected: /projects/B renders B's layout data (an ?_rsc fetch for /projects/B, or a fresh render of the layout with pid=B).
Observed: URL bar and <title> show B (metadata appears to refetch), but the rendered content is A's. The network panel shows no GET /projects/B?_rsc request at all for step 4 — the only activity is A's own subresources (in our app, an iframe still pointing at A's preview host). Reproduces both in vinext dev and consistently across pairs of params; order doesn't matter (B → back → A shows B under A's URL).
What we ruled out
- Server side is fine: when the
?_rsc request is issued (first visit to a pid, or hard load), the correct payload is returned.
key={pid} on the layout's client child doesn't fix it — the committed subtree is reused as-is, so the fresh keyed render never applies.
router.refresh() from a client guard "works" but races navigation: a popstate while the refresh's RSC stream is in flight hits abortSupersededNavigation → uncaught BodyStreamBuffer was aborted (stack below), and the stale subtree survives. Our current containment is a window.location.reload() when a client component sees useParams().pid disagree with its server props, which can't be superseded — but it's a full page load on every cross-param navigation into the route.
BodyStreamBuffer was aborted
abortSupersededNavigation vinext/dist/server/app-browser-entry.js:836:36
navigateRsc vinext/dist/server/app-browser-entry.js:843:4
<anonymous> vinext/dist/server/app-browser-popstate.js:29:49
<anonymous> vinext/dist/server/app-browser-entry.js:1181:3
(The useParams() context inside the reused subtree does update to B — only the server-rendered output is stale. That's what makes the client-side detection possible.)
Suspected area
Behaviorally this looks like the committed client cache / BFCache-style payload reuse treating the route pattern (/projects/[pid]) as the reuse key rather than the concrete URL, when the navigation goes dashboard → back → different param. Possibly related to the committed-payload reuse work (#2251) and history-entry restoration (#1366) — the back-traversal restores dashboard from cache, and the subsequent forward navigation to /projects/B appears to reuse /projects/A's committed entry instead of issuing a new RSC request.
Environment
- vinext
1.0.0-beta.1 (also reproduced on 0.2.0)
- vite
8.0.16, react 19.2.6, node 25.2.1, macOS (darwin 25.5.0)
vinext dev targeting Cloudflare Workers (workerd via miniflare)
Happy to provide more traces or test a patch — this bites any app where per-param data flows through a shared dynamic layout (in ours, the layout binds a per-project WebSocket + iframe, so the wrong project's editor renders under the new URL).
Summary
On soft navigation between two URLs that match the same shared dynamic layout but with different params (
/projects/A→ back →/projects/B), the client router reuses the committed subtree from/projects/Awithout fetching the RSC payload for/projects/B. The URL and<title>update to B, but everything rendered from the layout's server props (data fetched with A's param) remains A's. A layout-levelkey={param}does not help — the reused subtree is never re-rendered with fresh server output, so no new key is ever applied.Repro
App-router app with a shared dynamic layout that fetches per-param data:
Steps (client-side navigation throughout, e.g.
<Link>clicks + browser back):/dashboard./projects/A→ renders A correctly./dashboard./projects/B.Expected:
/projects/Brenders B's layout data (an?_rscfetch for/projects/B, or a fresh render of the layout withpid=B).Observed: URL bar and
<title>show B (metadata appears to refetch), but the rendered content is A's. The network panel shows noGET /projects/B?_rscrequest at all for step 4 — the only activity is A's own subresources (in our app, an iframe still pointing at A's preview host). Reproduces both invinext devand consistently across pairs of params; order doesn't matter (B → back → A shows B under A's URL).What we ruled out
?_rscrequest is issued (first visit to a pid, or hard load), the correct payload is returned.key={pid}on the layout's client child doesn't fix it — the committed subtree is reused as-is, so the fresh keyed render never applies.router.refresh()from a client guard "works" but races navigation: a popstate while the refresh's RSC stream is in flight hitsabortSupersededNavigation→ uncaughtBodyStreamBuffer was aborted(stack below), and the stale subtree survives. Our current containment is awindow.location.reload()when a client component seesuseParams().piddisagree with its server props, which can't be superseded — but it's a full page load on every cross-param navigation into the route.(The
useParams()context inside the reused subtree does update to B — only the server-rendered output is stale. That's what makes the client-side detection possible.)Suspected area
Behaviorally this looks like the committed client cache / BFCache-style payload reuse treating the route pattern (
/projects/[pid]) as the reuse key rather than the concrete URL, when the navigation goes dashboard → back → different param. Possibly related to the committed-payload reuse work (#2251) and history-entry restoration (#1366) — the back-traversal restores dashboard from cache, and the subsequent forward navigation to/projects/Bappears to reuse/projects/A's committed entry instead of issuing a new RSC request.Environment
1.0.0-beta.1(also reproduced on0.2.0)8.0.16, react19.2.6, node25.2.1, macOS (darwin 25.5.0)vinext devtargeting Cloudflare Workers (workerd via miniflare)Happy to provide more traces or test a patch — this bites any app where per-param data flows through a shared dynamic layout (in ours, the layout binds a per-project WebSocket + iframe, so the wrong project's editor renders under the new URL).