diff --git a/packages/vinext/src/server/app-browser-entry.ts b/packages/vinext/src/server/app-browser-entry.ts index 1c40bcd21..34ba34822 100644 --- a/packages/vinext/src/server/app-browser-entry.ts +++ b/packages/vinext/src/server/app-browser-entry.ts @@ -1036,7 +1036,7 @@ function bootstrapHydration(rscStream: ReadableStream): void { let redirectCount = redirectDepth; try { - const shouldUsePendingRouterState = programmaticTransition && navigationKind !== "refresh"; + const shouldUsePendingRouterState = programmaticTransition; if (shouldUsePendingRouterState && hasBrowserRouterState()) { pendingRouterState = beginPendingBrowserRouterState(); } else { diff --git a/tests/e2e/app-router/nextjs-compat/router-push-pending.spec.ts b/tests/e2e/app-router/nextjs-compat/router-push-pending.spec.ts index 2d3e010e4..d9634d920 100644 --- a/tests/e2e/app-router/nextjs-compat/router-push-pending.spec.ts +++ b/tests/e2e/app-router/nextjs-compat/router-push-pending.spec.ts @@ -1,11 +1,13 @@ /** - * Next.js Compat E2E: router push pending state + * Next.js Compat E2E: router pending state * * Next.js references: * - https://github.com/vercel/next.js/blob/canary/test/e2e/use-link-status/index.test.ts * - https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/navigation/navigation.test.ts * - https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/navigation/navigation.test.ts#L482 * (redirect-with-loading: verifies redirect only triggers once and does not flicker) + * - https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/app-router-instance.ts + * - https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/use-action-queue.ts * * The contract we care about here is that a programmatic App Router navigation * started inside useTransition should flip isPending immediately and keep it @@ -17,7 +19,7 @@ import { waitForAppRouterHydration } from "../../helpers"; const BASE = "http://localhost:4174"; -test.describe("Next.js compat: router.push pending state (browser)", () => { +test.describe("Next.js compat: router pending state (browser)", () => { test("same-route search param push keeps useTransition pending until commit", async ({ page, }) => { @@ -136,4 +138,28 @@ test.describe("Next.js compat: router.push pending state (browser)", () => { // Final state: URL is at destination expect(page.url()).toContain("/nextjs-compat/router-push-pending-destination"); }); + + test("router.refresh keeps useTransition pending until the refreshed route commits", async ({ + page, + }) => { + await page.goto(`${BASE}/nextjs-compat/router-refresh-pending`); + await waitForAppRouterHydration(page); + + await expect(page.locator("#refresh-pending-state")).toHaveText("idle"); + const initialStamp = await page.locator("#refresh-server-stamp").textContent(); + expect(initialStamp).toBeTruthy(); + + const clickPromise = page.click("#refresh-current-route", { noWaitAfter: true }); + + await expect(page.locator("#refresh-pending-state")).toHaveText("pending", { + timeout: 1_000, + }); + await clickPromise; + await expect(page.locator("#refresh-server-stamp")).not.toHaveText(initialStamp ?? "", { + timeout: 10_000, + }); + await expect(page.locator("#refresh-pending-state")).toHaveText("idle", { + timeout: 10_000, + }); + }); }); diff --git a/tests/fixtures/app-basic/app/nextjs-compat/router-refresh-pending/page.tsx b/tests/fixtures/app-basic/app/nextjs-compat/router-refresh-pending/page.tsx new file mode 100644 index 000000000..36332acc4 --- /dev/null +++ b/tests/fixtures/app-basic/app/nextjs-compat/router-refresh-pending/page.tsx @@ -0,0 +1,19 @@ +import { RefreshPendingClient } from "./refresh-pending-client"; + +async function delay(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export default async function RouterRefreshPendingPage() { + // Keep the refresh RSC request in flight long enough for the browser test to + // observe the transition's pending state before the refreshed tree commits. + await delay(1_000); + + return ( +
+

Router refresh pending

+ +

server stamp: {Date.now()}

+
+ ); +} diff --git a/tests/fixtures/app-basic/app/nextjs-compat/router-refresh-pending/refresh-pending-client.tsx b/tests/fixtures/app-basic/app/nextjs-compat/router-refresh-pending/refresh-pending-client.tsx new file mode 100644 index 000000000..6f43add5b --- /dev/null +++ b/tests/fixtures/app-basic/app/nextjs-compat/router-refresh-pending/refresh-pending-client.tsx @@ -0,0 +1,25 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useTransition } from "react"; + +export function RefreshPendingClient() { + const router = useRouter(); + const [isPending, startTransition] = useTransition(); + + return ( +
+

{isPending ? "pending" : "idle"}

+ +
+ ); +}