Skip to content

App Router: soft navigation between different dynamic params reuses the committed shared-layout subtree without fetching the new RSC payload #2597

Description

@eashish93

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):

  1. Load /dashboard.
  2. Click a link to /projects/A → renders A correctly.
  3. Browser back to /dashboard.
  4. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions