mirror of
https://github.com/cloudflare/vinext.git
synced 2026-05-09 08:25:34 +02:00
[PR #870] [MERGED] fix(app-router): keep isPending true across RSC-level redirects #910
Labels
No labels
enhancement
enhancement
good first issue
help wanted
nextjs-tracking
nextjs-tracking
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
starred/vinext#910
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/cloudflare/vinext/pull/870
Author: @NathanDrake2406
Created: 4/22/2026
Status: ✅ Merged
Merged: 4/22/2026
Merged by: @james-elicx
Base:
main← Head:fix/app-router-redirect-inline-loop📝 Commits (2)
6edaeb2test(app-router): add E2E coverage for isPending across RSC redirect01079cbfix(app-router): keep isPending true across RSC-level redirects📊 Changes
5 files changed (+320 additions, -219 deletions)
View changed files
📝
packages/vinext/src/server/app-browser-entry.ts(+199 -219)📝
tests/e2e/app-router/nextjs-compat/router-push-pending.spec.ts(+94 -0)➕
tests/fixtures/app-basic/app/nextjs-compat/router-push-pending-destination/page.tsx(+9 -0)➕
tests/fixtures/app-basic/app/nextjs-compat/router-push-pending-redirect/page.tsx(+8 -0)📝
tests/fixtures/app-basic/app/nextjs-compat/router-push-pending/pending-client.tsx(+10 -0)📄 Description
What this changes
router.push('/a')where/a's server component callsredirect('/b')no longer causesuseTransition().isPendingto flashfalsemid-redirect. The pending indicator staystruecontinuously from the push through the final destination commit.Why
PR #868 latched
isPendingduring programmatic navigations via a deferred promise in browser router state. But when the RSC response redirected (307),navigateRscdetected the response URL differing from the requested URL, settled the deferred promise to avoid orphaning it, then recursed withprogrammaticTransition=false. The settle fired before the recursive call, so React resolved the transition andisPendingdropped tofalseduring the hop.The bug was explicitly documented in the bandage comment before the recursive call:
Issue #869 asked to remove that tradeoff.
Approach
Convert the recursive redirect follow into an inline
while(true)loop insidenavigateRsc. Loop variables (currentHref,currentHistoryMode,currentPrevNextUrl,redirectCount) are updated on each redirect hop; the loopcontinues without re-enteringnavigateRsc. A singlependingRouterStateis created before the loop and lives until thefinallyblock, soisPendingstaystrueacross all hops.The scattered
settlePendingBrowserRouterState(pendingRouterState)calls inside the try body (early-return stale-id guards) are removed. Thefinallyowns the single settlement site and is idempotent via thesettledflag onPendingBrowserRouterState.Scope: this is the minimal fix described in issue #869. The
NavigationTask-based refactor innathan/navigation-architecture.mdis explicitly out of scope.Validation
New E2E test (
router-push-pending.spec.ts) installs aMutationObserverbefore click, records every text value of#pending-state, and asserts no"idle"entry appears between the first"pending"and the destination commit.Test log on unmodified code:
["idle","pending","idle","__removed__"]-- the mid-redirect flash at index 2 fails the assertion.Test log after fix:
["idle","pending","__removed__"]-- no idle flash.vp check: cleanPorted test from Next.js:
https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/navigation/navigation.test.ts#L482
Closes #869.
Risks / follow-ups
redirectDepthparameter onnavigateRscis now vestigial (no caller passes non-zero since recursion is gone). Left as-is to avoid an unnecessary signature churn; can be cleaned up in a separate pass.isPendingcorrectness only.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.