From 2b6b7254b70848007250afe20f0833adadcaaa89 Mon Sep 17 00:00:00 2001 From: Zack Tanner <1939140+ztanner@users.noreply.github.com> Date: Wed, 7 Aug 2024 17:50:28 -0700 Subject: [PATCH] fix app router prefetch deduping --- packages/next/src/client/link.tsx | 7 ++-- .../app-prefetch/app/prefetch-race/page.js | 19 +++++++++++ .../app-dir/app-prefetch/prefetching.test.ts | 33 ++++++++++++++++++- 3 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 test/e2e/app-dir/app-prefetch/app/prefetch-race/page.js diff --git a/packages/next/src/client/link.tsx b/packages/next/src/client/link.tsx index d10cc2f6670b0..8063ddd07a14c 100644 --- a/packages/next/src/client/link.tsx +++ b/packages/next/src/client/link.tsx @@ -148,8 +148,11 @@ function prefetch( } // We should only dedupe requests when experimental.optimisticClientCache is - // disabled. - if (!options.bypassPrefetchedCheck) { + // disabled & when we're not using the app router. App router handles + // reusing an existing prefetch entry (if it exists) for the same URL. + // If we dedupe in here, we will cause a race where different prefetch kinds + // to the same URL (ie auto vs true) will cause one to be ignored. + if (!options.bypassPrefetchedCheck && !isAppRouter) { const locale = // Let the link's locale prop override the default router locale. typeof options.locale !== 'undefined' diff --git a/test/e2e/app-dir/app-prefetch/app/prefetch-race/page.js b/test/e2e/app-dir/app-prefetch/app/prefetch-race/page.js new file mode 100644 index 0000000000000..e01e7a2d0f9ea --- /dev/null +++ b/test/e2e/app-dir/app-prefetch/app/prefetch-race/page.js @@ -0,0 +1,19 @@ +import Link from 'next/link' +import React from 'react' + +export default function Page() { + return ( + <> +
+ + /force-dynamic/test-page (prefetch: auto) + +
+
+ + /force-dynamic/test-page (prefetch: true) + +
+ + ) +} diff --git a/test/e2e/app-dir/app-prefetch/prefetching.test.ts b/test/e2e/app-dir/app-prefetch/prefetching.test.ts index df00f5ea86355..f3ec1101cbf26 100644 --- a/test/e2e/app-dir/app-prefetch/prefetching.test.ts +++ b/test/e2e/app-dir/app-prefetch/prefetching.test.ts @@ -1,6 +1,6 @@ import { nextTestSetup } from 'e2e-utils' import { check, waitFor, retry } from 'next-test-utils' -import type { Page, Request } from 'playwright' +import type { Page, Request, Route } from 'playwright' import { NEXT_RSC_UNION_QUERY } from 'next/dist/client/components/app-router-headers' const browserConfigWithFixedTime = { @@ -462,5 +462,36 @@ describe('app dir - prefetching', () => { expect(dashboardRequests[1].priority).toBe('auto') // the second request is the lazy fetch to fill in missing data }) }) + + it('should respect multiple prefetch types to the same URL', async () => { + let interceptRequests = false + + const browser = await next.browser('/prefetch-race', { + beforePageLoad(page: Page) { + page.route('**/force-dynamic/**', async (route: Route) => { + if (!interceptRequests) { + return route.continue() + } + + const request = route.request() + const headers = await request.allHeaders() + + if (headers['rsc'] === '1') { + // intentionally stall the request, + // as after the initial page load, there shouldn't be any additional fetches + // since the data should already be available. + } else { + await route.continue() + } + }) + }, + }) + + await browser.waitForIdleNetwork() + interceptRequests = true + + await browser.elementByCss('[href="/force-dynamic/test-page"]').click() + await browser.waitForElementByCss('#test-page') + }) }) })