Skip to content

fix(app-router): settle superseded link navigations - #2025

Merged
james-elicx merged 1 commit into
mainfrom
codex/fix-use-link-status-imperative-navigation
Jun 15, 2026
Merged

fix(app-router): settle superseded link navigations#2025
james-elicx merged 1 commit into
mainfrom
codex/fix-use-link-status-imperative-navigation

Conversation

@james-elicx

Copy link
Copy Markdown
Member

Summary

  • clear link-owned pending state when imperative router.push or router.replace supersedes a link navigation
  • abort superseded live RSC requests and prevent stale commits/cache writes
  • add vinext-owned rapid-click and imperative-navigation coverage

Next.js parity

Fixes failures from test/e2e/use-link-status/index.test.ts, including pending state surviving router.push and rapid overlapping navigations.

Validation

  • pinned Next.js v16.2.6 deploy suite: 6/6 passed
  • focused unit tests: 324 passed
  • new Playwright fixture: 2 passed
  • adjacent navigation tests: 16 passed
  • scoped vp check and vinext build
  • two review passes; no actionable findings remain

@pkg-pr-new

pkg-pr-new Bot commented Jun 15, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@2025
npm i https://pkg.pr.new/vinext@2025

commit: 1b28529

@github-actions

Copy link
Copy Markdown
Contributor
Example Preview Production Original
app-router-cloudflare preview production
pages-router-cloudflare preview production
app-router-playground preview production original
realworld-api-rest preview production
nextra-docs-template preview production
benchmarks preview production
hackernews preview production original
workers-cache preview production
web preview production

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

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.

Review: settle superseded link navigations

I traced the two changes against the navigation runtime, the link-status registry, and the existing navId-supersession guards. The implementation is sound and well-scoped. No blocking issues.

What works correctly

shims/navigation.ts — synchronous notifyLinkNavigationStart() in push/replace

The comment captures the real motivation: clearing the link-owned pending state before React.startTransition so React doesn't defer the idle setPending(false) behind the suspended destination render. The double-call (synchronous here + the existing call inside navigateClientSide) is safe:

  • Link-initiated nav: setLinkForCurrentNavigation sets currentNavigationIsLinkInitiated = true; the first notifyLinkNavigationStart() consumes the marker and keeps the link pending.
  • Interrupting router.push: the synchronous call (marker already false) calls linkSetterForMostRecentNavigation(false) and nulls the ref; the second call inside navigateClientSide is a no-op.

This matches the registry's documented semantics (link-status-registry.ts:62) and is covered by tests/link-status-registry.test.ts.

server/app-browser-entry.ts — per-navigation AbortController

  • activeNavigationAbortController?.abort() at navigation start actively cancels the superseded live RSC fetch instead of merely ignoring its result via the navId guard — a genuine improvement.
  • The aborted fetch throws AbortError, lands in the catch, and the isCurrentNavigation(navId) guard (line 2222) returns early, so no stale commit or hard-nav to a stale URL.
  • The finally reset is correctly guarded (if (activeNavigationAbortController === navigationAbortController)), so a newer navigation's controller is never clobbered.
  • The controller correctly spans redirect-following hops (reused across the internal continue loop) and is freed after the final navigation completes — no dangling reference.
  • void cacheBufferPromise.catch(() => {}) (line 2156) prevents an unhandled rejection when the teed cache branch rejects on an aborted/superseded stream before the early-return at line 2158 — a good defensive add.

Non-blocking observations

  1. No unit coverage for the App Router abort path. The new abort logic is exercised only by the added Playwright fixture. The Pages Router has a dedicated unit test (tests/shims.test.ts:16040) that asserts the abort signal fires and AbortError is converted; an equivalent focused test for the App Router navigateRsc path would harden against regressions, given how much subtle supersession logic lives in app-browser-entry.ts. Not required for this PR.

  2. router.refresh() does not clear sticky link-pending state. refresh() calls the navigation runtime's navigate (navigateRsc) directly rather than navigateClientSide, so it never invokes notifyLinkNavigationStart(). A refresh() that interrupts a pending <Link> click would leave the link's useLinkStatus() indicator stuck. This is pre-existing and outside the scope of this PR (which targets imperative push/replace per the title and the ported Next.js test), but worth a follow-up to confirm Next.js parity — Next.js's refresh-reducer also begins a navigation the tracked link did not initiate.

Validation performed

  • vp check on both modified source files: pass
  • tests/link-status-registry.test.ts, tests/link-navigation.test.ts, tests/navigation-runtime.test.ts, link-scoped tests/link.test.ts / tests/shims.test.ts: all pass (60+ tests)

LGTM.

@ask-bonk

ask-bonk Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2025.

Summary

I reviewed PR #2025 ("settle superseded link navigations") by tracing both changes against the navigation runtime, the link-status registry, and the existing navId-supersession guards. No blocking issues — the implementation is correct and well-scoped.

Findings

Both changes are sound:

  • shims/navigation.ts: The synchronous notifyLinkNavigationStart() in push/replace (before startTransition) correctly clears link-owned pending state. The resulting double-call is safe given the registry's marker-consumption semantics.
  • server/app-browser-entry.ts: The per-navigation AbortController actively cancels superseded live RSC fetches, the finally reset is properly guarded against clobbering a newer navigation's controller, the controller correctly spans redirect hops, and void cacheBufferPromise.catch(() => {}) prevents an unhandled rejection on aborted streams.

Two non-blocking observations:

  1. No unit coverage for the new App Router abort path (only the Playwright fixture) — the Pages Router has an equivalent unit test that could be mirrored.
  2. router.refresh() doesn't clear sticky link-pending state (it bypasses navigateClientSide) — but this is pre-existing and outside this PR's scope.

I verified vp check passes on both files and ran the navigation/link unit suites (60+ tests, all passing).

github run

@james-elicx
james-elicx marked this pull request as ready for review June 15, 2026 10:31
@james-elicx
james-elicx merged commit fc8e6f0 into main Jun 15, 2026
55 checks passed
@james-elicx
james-elicx deleted the codex/fix-use-link-status-imperative-navigation branch June 15, 2026 20:51
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.

1 participant