Skip to content

fix(router): derive shallow dynamic params from the URL#1272

Merged
james-elicx merged 3 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/fix-shallow-dynamic-query
May 16, 2026
Merged

fix(router): derive shallow dynamic params from the URL#1272
james-elicx merged 3 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/fix-shallow-dynamic-query

Conversation

@NathanDrake2406
Copy link
Copy Markdown
Contributor

Overview

Item Detail
Goal Keep Pages Router dynamic params aligned with the browser URL during shallow navigation.
Core change Match the current pathname against __NEXT_DATA__.page before falling back to hydrated __NEXT_DATA__.query.
Main boundary Pages Router client router state, specifically router.query for dynamic templates.
Primary files packages/vinext/src/shims/router.ts, tests/shims.test.ts, tests/e2e/pages-router/shallow-routing.spec.ts
Expected impact /posts/42 to /posts/43 with { shallow: true } now reports router.query.id === "43" without refetching page props.

Why

For Pages Router shallow navigation to be correct, the router state must reflect the visible URL even when data fetching is intentionally skipped. The previous implementation reused route params from the initial hydration payload, so same-template dynamic shallow transitions changed asPath but left query stale.

Area Principle / invariant What this PR changes
Dynamic route params Params are part of the current route state, not immutable hydration data. Derives params from the current window.location.pathname when it matches the hydrated route pattern.
Shallow routing Data fetching should be skipped, but router state should still update. Preserves the existing shallow no-fetch path while updating router.query.
Compatibility Dynamic params should override same-key search params. Keeps route params merged after search params, preserving the existing precedence.

What changed

Scenario Before After
/posts/42 shallow-pushes to /posts/43 router.query stayed { id: "42" } router.query becomes { id: "43" }
Current path cannot be matched to the route pattern Hydrated route params were used Same fallback remains in place
Catch-all route params Read from hydrated query only Can be reconstructed from the current path when matched
Maintainer review path
  1. packages/vinext/src/shims/router.ts: review the route-pattern parsing and fallback path in getRouteQueryFromNextData.
  2. tests/shims.test.ts: review the focused red/green router-state regression that leaves __NEXT_DATA__.query intentionally stale.
  3. tests/e2e/pages-router/shallow-routing.spec.ts: review the browser-visible regression proving props stay shallow-frozen while router.query updates.
Validation
  • vp test run tests/shims.test.ts -t "updates dynamic route params from the URL after shallow navigation"
  • vp test run tests/shims.test.ts -t "Pages Router router"
  • vp test run tests/shims.test.ts
  • PLAYWRIGHT_PROJECT=pages-router pnpm exec playwright test tests/e2e/pages-router/shallow-routing.spec.ts -g "dynamic route params update"
  • vp check
Risk / compatibility
  • Public API: preserves the existing Pages Router API shape.
  • Runtime: client-only router state derivation changes for dynamic routes after URL changes.
  • Existing apps: intended to improve compatibility with Next.js shallow dynamic routing. If the current URL does not match the hydrated route pattern, vinext still falls back to hydrated params.
  • Build output: no server build or route manifest changes.
Non-goals
  • This does not change the broader vinext behavior that trusts { shallow: true } to skip client fetching.
  • This does not refactor Pages Router route matching into shared runtime infrastructure.

References

Reference Why it matters
Next.js router merges dynamic route matches into query Shows params are parsed from the destination path, not read from initial hydration data.
Next.js shallow route-info reuse Shows shallow navigation can reuse existing page info while router state still updates separately.
Next.js dynamic shallow-routing test Expects a dynamic shallow push to update next.router.query.slug from first to second.
Next.js shallow routing docs Documents that shallow routing updates pathname and query without rerunning data fetching methods.

Pages Router shallow navigation across one dynamic route template updated the browser URL but kept router.query params from the original __NEXT_DATA__ payload. That left client state stale after transitions such as /posts/42 to /posts/43.

The router now matches the current pathname against the hydrated route pattern before falling back to __NEXT_DATA__.query. This preserves shallow data-fetch skipping while keeping dynamic params aligned with the visible URL.

Tests cover the router shim boundary and the browser-visible Pages Router shallow transition.
@NathanDrake2406 NathanDrake2406 marked this pull request as ready for review May 16, 2026 13:15
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 16, 2026

Open in StackBlitz

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

commit: d4542e1

The shallow dynamic params fix originally duplicated route-pattern parsing inside the router shim.

Reuse the existing routePatternParts and matchRoutePattern helpers instead, keeping the __NEXT_DATA__ fallback behavior unchanged.
@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.

Clean, well-scoped fix. The approach of deriving dynamic params from the current URL via matchRoutePattern (with a fallback to hydrated __NEXT_DATA__.query) is correct and matches how Next.js handles this in router.ts lines 1537-1590.

The layering is good:

  1. Try to match window.location.pathname against __NEXT_DATA__.page pattern — this handles the shallow navigation case where the URL changed but __NEXT_DATA__.query is stale.
  2. Fall back to reading param names from __NEXT_DATA__.query — this covers edge cases where the URL doesn't match the pattern (e.g., middleware rewrites that change the visible URL shape).

Reusing matchRoutePattern + routePatternParts from routing/route-pattern.ts avoids duplicating pattern-matching logic. The decodeMatchedParams call inside matchRoutePattern correctly handles percent-encoded URL segments.

Tests are thorough: the unit test verifies that __NEXT_DATA__.query stays stale (confirming no mutation) while router.query reflects the new URL, and the E2E test confirms the full browser scenario including that props stay frozen (shallow) while router state updates.

Two minor observations below, neither blocking.

@@ -300,6 +301,46 @@ function extractRouteParamNames(pattern: string): string[] {
return names;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: extractRouteParamNames is now only called inside getRouteQueryFromNextData (the fallback path). If you wanted to tighten things up in a follow-up, you could consider inlining it there or just noting that it's exclusively used for the fallback. Not blocking — the current structure is clear enough.

Comment thread tests/shims.test.ts
Comment on lines +10371 to +10375
__NEXT_DATA__: {
page: "/posts/[id]",
query: { id: "42" },
isFallback: false,
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nice detail: the test intentionally keeps __NEXT_DATA__.query stale at { id: "42" } and asserts it stays unchanged (line 10397), which proves the fix derives params from the URL rather than mutating the hydration payload. This is exactly the right thing to verify.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 16, 2026

Review submitted and approved. Here's a summary of my findings:

Verdict: Approve

The fix correctly addresses stale router.query during shallow navigation between dynamic routes (e.g., /posts/42/posts/43 with { shallow: true }). The approach:

  1. Primary path: Matches window.location.pathname against the route pattern from __NEXT_DATA__.page using the existing matchRoutePattern + routePatternParts infrastructure — no duplicated logic.
  2. Fallback path: If the URL doesn't match the pattern (e.g., middleware rewrites), falls back to reading params from __NEXT_DATA__.query (the old behavior).
  3. decodeMatchedParams inside matchRoutePattern correctly handles percent-encoded URL segments.

This matches how Next.js handles it in their router.ts (lines 1537-1590), where params are parsed from the destination path rather than read from hydration data.

Tests are well-structured — the unit test proves __NEXT_DATA__.query stays stale while router.query reflects the new URL, and the E2E test validates the full browser scenario with both frozen props and updated router state.

github run

…namic-query

# Conflicts:
#	packages/vinext/src/shims/router.ts
@james-elicx james-elicx enabled auto-merge (squash) May 16, 2026 18:56
@james-elicx james-elicx merged commit be9f338 into cloudflare:main May 16, 2026
26 checks passed
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.

2 participants