From 944a84c2095b79a1f528656855f952625ca875b2 Mon Sep 17 00:00:00 2001 From: Zack Tanner Date: Mon, 11 Mar 2024 14:55:04 -0700 Subject: [PATCH] ensure mpa navigations to the same URL work after restoring from bfcache (#63155) ### What When triggering an MPA navigation (also commonly referred to as a "hard navigation"), and then restoring the previous page via the browser's bfcache, subsequent requests to the same link wouldn't navigate until reloading the page or performing a different navigation. ### Why MPA navigations in app router are handled in a fairly unconventional way: the router state is updated with an indication that an external URL was clicked, and once the router sees the pending navigation, it kicks off a `location.replace` or `location.push` with the specified URL **in render**. The router then suspends indefinitely to prevent committing the render. However, the router will only make the `replace`/`push` request if there's not already a pending navigation to that same URL. The pending check is needed to avoid continuously calling `push`/`replace` when unrelated router state changes occur (for example, if I hover over a link and trigger a prefetch action and the router re-renders, it shouldn't make another `location.push` call to the same URL that's pending) However, the source of the bug is that the variable that holds this pending state is also restored by the browser's cache, since it takes a snapshot prior to exiting the page. This means that when clicking the browser back button, `pendingMpaPath` would still be set to the URL we just came from. When clicking the link again, it would see that the requested URL is the same as the pending URL, and not perform any history actions. ### How This clears the pending value when the router is restored from bfcache. [slack x-ref](https://vercel.slack.com/archives/C0676QZBWKS/p1710169967246929) Closes NEXT-2781 Closes NEXT-2776 --- packages/next/src/client/components/app-router.tsx | 5 +++++ test/production/bfcache-routing/index.test.ts | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/packages/next/src/client/components/app-router.tsx b/packages/next/src/client/components/app-router.tsx index 48058b833f21c..66fd66d07eca7 100644 --- a/packages/next/src/client/components/app-router.tsx +++ b/packages/next/src/client/components/app-router.tsx @@ -417,6 +417,11 @@ function Router({ return } + // Clear the pendingMpaPath value so that a subsequent MPA navigation to the same URL can be triggered. + // This is necessary because if the browser restored from bfcache, the pendingMpaPath would still be set to the value + // of the last MPA navigation. + globalMutable.pendingMpaPath = undefined + dispatch({ type: ACTION_RESTORE, url: new URL(window.location.href), diff --git a/test/production/bfcache-routing/index.test.ts b/test/production/bfcache-routing/index.test.ts index f813c720bd9ab..4df1876bbabc8 100644 --- a/test/production/bfcache-routing/index.test.ts +++ b/test/production/bfcache-routing/index.test.ts @@ -69,5 +69,15 @@ describe('bfcache-routing', () => { // we should be back on the test page with no errors html = await browser.evalAsync('document.documentElement.innerHTML') expect(html).toContain('BFCache Test') + + // After restoring from bfcache, a subsequent mpa navigation to the same URL should work + // We trigger the click via `evalAsync` because when restoring from bfcache, our internal + // 'waitForElementByCss' method doesn't think the element is attached to the DOM. + await browser.evalAsync( + `document.querySelector('a[href="https://example.vercel.sh"]').click()` + ) + await browser.waitForCondition( + 'window.location.origin === "https://example.vercel.sh"' + ) }) })