Skip to content

fix(app-router): restore shallow pathname on history traversal - #2829

Open
GtechGovind wants to merge 4 commits into
cloudflare:mainfrom
GtechGovind:agent/preserve-shallow-history-pathname
Open

fix(app-router): restore shallow pathname on history traversal#2829
GtechGovind wants to merge 4 commits into
cloudflare:mainfrom
GtechGovind:agent/preserve-shallow-history-pathname

Conversation

@GtechGovind

Copy link
Copy Markdown

Summary

  • persist the destination URL on external history.pushState and history.replaceState entries
  • treat marked history traversal entries as shallow URL restores so App Router keeps the current rendered tree
  • synchronize usePathname and useSearchParams when popstate takes the same-route fast path
  • add Next.js-compatible browser back/forward regression coverage

Root cause

External History API calls updated Vinext's navigation-hook URL cache, but the App Router popstate fast path only restored history metadata and scroll position. It did not resynchronize the hooks when traversing back to the rendered route. The pushed entry also lacked a marker that would let forward traversal reuse the current tree instead of attempting an RSC navigation.

The new internal shallow URL metadata identifies those entries, allowing popstate to synchronize the URL-backed hooks while preserving the current App Router tree.

Fixes #1541.

Validation

  • ./node_modules/.bin/vp check
  • ./node_modules/.bin/vp test run tests/shims.test.ts -t 'preserves App Router history metadata when external history calls provide caller state'
  • PLAYWRIGHT_PROJECT=app-router ./node_modules/.bin/playwright test tests/e2e/app-router/advanced.spec.ts -g 'Shallow Routing' (4 passed, 1 existing fixme skipped)

@GtechGovind
GtechGovind marked this pull request as ready for review August 6, 2026 17:45

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

Copy link
Copy Markdown

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

ℹ️ 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 thread packages/vinext/src/server/app-browser-entry.ts Outdated
@GtechGovind
GtechGovind marked this pull request as draft August 8, 2026 05:57
@GtechGovind
GtechGovind marked this pull request as ready for review August 8, 2026 06:42
@GtechGovind

Copy link
Copy Markdown
Author

@codex review

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

Copy link
Copy Markdown

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: 126007f9e4

ℹ️ 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".

this.#pushHistoryState(historyState, options.href);
}
this.commitHistoryTraversalIndex(navigationHistoryIndex);
this.rememberHistoryStateSnapshot(options.snapshotState);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve shallow snapshots across cache invalidation

When a shallow entry uses a pathname that is not itself a valid route, storing its tree only in the bounded, invalidatable snapshot cache makes the entry stop working after router.refresh() or cache eviction. For example, push /shallow-test/sub, navigate to /about, refresh there, and then go Back: clearClientNavigationCaches() has removed this snapshot, so popstate falls through to an RSC request for nonexistent /shallow-test/sub instead of restoring the Shallow Routing Test tree. The shallow entry needs a restoration mechanism that survives general navigation-cache invalidation.

Useful? React with 👍 / 👎.

Comment thread packages/vinext/src/shims/navigation.ts Outdated
createExternalHistoryStatePreservingMetadata(
data,
window.history.state,
new URL(url ?? window.location.href, window.location.href).href,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve caller state in the non-App-Router fallback

When no App Router state is available—normally on a Pages Router page or before hydration—passing shallowUrl prevents createExternalHistoryStatePreservingMetadata() from returning the caller's data unchanged. It instead clones the value into a plain record, so pushState(42, ...), arrays, dates, and other structured-cloneable values no longer round-trip through history.state; non-cloneable values such as functions can also be discarded rather than producing the native DataCloneError. Avoid adding the App Router shallow marker on this fallback path, or preserve arbitrary history data without coercing it to a record.

Useful? React with 👍 / 👎.

@GtechGovind
GtechGovind marked this pull request as draft August 8, 2026 11:34
@GtechGovind
GtechGovind marked this pull request as ready for review August 8, 2026 12:18

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

Copy link
Copy Markdown

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: 5e4bdabd84

ℹ️ 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 +71 to +73
// A normal navigation replacing the same traversal entry supersedes any
// shallow restoration state previously associated with that index.
this.#durableSnapshots.delete(options.historyIndex);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep shallow snapshots durable after their first restoration

Fresh evidence after the prior cache-invalidation fix: when Back restores a durable shallow entry, BrowserRoot renders that stored tree and its useLayoutEffect calls rememberHistoryStateSnapshot() without durable, so this line immediately demotes the entry into the bounded cache. After navigating Forward, invalidating caches (for example with router.refresh()), and going Back again, the snapshot is gone and the router requests the potentially nonexistent shallow pathname. A shallow entry must retain its durable status when the ordinary render-snapshot effect observes the same history index.

Useful? React with 👍 / 👎.

if (!browserNavigationController.hasBrowserRouterState()) {
return false;
}
const href = new URL(url ?? window.location.href, window.location.href).href;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve relative URLs for native shallow history writes

On a Chromium document loaded with URL userinfo, such as http://user:pass@host/, window.location.href strips the userinfo while document.URL retains it (the existing #2614 hydration regression covers this). Resolving a caller's relative pushState/replaceState URL against location.href and forwarding this credential-free absolute href to the controller makes the native history operation throw SecurityError, whereas passing the caller's relative URL succeeds. Use the absolute URL for the navigation snapshot only, while preserving the original URL for the native history write.

Useful? React with 👍 / 👎.

Comment on lines +65 to +67
if (options.durable === true) {
this.#snapshots.delete(options.historyIndex);
this.#durableSnapshots.set(options.historyIndex, options.state);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Bound retention of durable history snapshots

Every hydrated external history.pushState() adds a complete AppRouterState to this map, but maxEntries and clear() apply only to #snapshots; durable entries are never evicted unless the same traversal index is later remembered normally. Repeated shallow pushes therefore retain entire rendered trees for the document lifetime, including entries made unreachable when the user goes Back and pushes a new history branch. Add explicit pruning or bounded ownership for durable snapshots while preserving entries that remain traversable.

AGENTS.md reference: AGENTS.md:L477-L483

Useful? React with 👍 / 👎.

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.

App Router: history.pushState with new pathname not reflected after back/forward

1 participant