mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
a775527b8b
* fix(app-router): track pending router state for programmatic navigations `router.push()` / `router.replace()` / `router.refresh()` dispatched the new tree only after the RSC payload arrived, so `useTransition().isPending` dropped to false the moment the user callback returned and Suspense boundaries visibly committed their fallbacks during the fetch window. Wrapping the programmatic call in `useTransition` did not help because the state update React needed to latch onto was not scheduled until the navigation was already committed. Issue #639 describes the downstream symptom: double-flashing Suspense fallbacks during client navigation. Root cause: the browser router state was a `useReducer` that was only dispatched at commit time. React had no in-flight promise to wait on, so transition priority could not keep the previous UI on screen. Switch the browser router state to `useState<State | Promise<State>>` and, when a programmatic navigation starts, publish a deferred promise into the state slot synchronously. `BrowserRoot` unwraps the slot via `use()`, so any render during the RSC fetch suspends with transition priority and the previous UI stays visible. The deferred promise is resolved with the reducer output when the payload dispatches, or settled with the current committed state on supersession / error / internal RSC redirect, so `useTransition().isPending` latches true for the full navigation window and drops exactly once at commit. `router.push` / `replace` / `refresh` now also wrap their synchronous entry in `React.startTransition`, matching Next.js's `app-router-instance.ts`. Covered by `tests/e2e/app-router/nextjs-compat/router-push-pending.spec.ts`: same-route search-param push inside `useTransition` must keep the pending flag true until the server component commits its new filter. Known limitation (tracked as a follow-up): the internal RSC-redirect path still recurses through `__VINEXT_RSC_NAVIGATE__` with `programmaticTransition=false`, so the outer pending must be settled before the recursion. `useTransition().isPending` flashes false mid-redirect until the recursive call finishes. Fixing this requires converting the recursive redirect into an inline loop inside `navigateRsc` so a single deferred promise can span all hops, the way Next.js's action queue does. * address review feedback on programmatic router pending state Replace duck-type "then" in value check with instanceof Promise in isRouterStatePromise — the promises are always created locally so instanceof is correct and avoids misclassification if AppRouterState ever gains a then property. Add a comment in dispatchBrowserTree explaining why the pendingRouterState early return safely ignores the useTransitionMode branch — the outer React.startTransition in router.push/replace/refresh already provides transition priority, so no additional wrap is needed. Remove dead typeof React.startTransition === "function" guards in push/replace/refresh — vinext requires React 19+, where startTransition is always present. Inline the transition call directly. Bump SlowFilterResult delay from 200ms to 1_000ms so the isPending assertion has a wider window and is less susceptible to CI timing races.