Skip to content

Commit 9673eeb

Browse files
committed
[Link] Add prefetch="auto" option
Adds a "auto" as a valid value for the `<Link>` component's prefetch prop. It acts as an alias for the default behavior, i.e. same as no prefetch prop at all. --- A common source of confusion is the difference between `<Link>`, `<Link prefetch={null}>`, and `<Link prefetch={boolean}>`: - `prefetch={true}` → dynamically prefetch the whole page - `prefetch={false}` → disable prefetching entirely - no prop, `prefetch={null}`, `prefetch={undefined}` → default behavior If you want to conditionally opt into dynamic prefetching, you have to write something like this: ``` <Link prefetch={shouldDynamicallyPrefetch ? true : null}> ``` After this PR, you can do this instead: ``` <Link prefetch={shouldDynamicallyPrefetch ? true : 'auto'}> ``` It's still a bit confusing if you aren't aware of the subtleties of dynamic versus static prefetching, but at least the "auto" string gives slightly more of a hint that `true` is different from the default. (In retrospect, we probably should have made `true` an alias for the default, and chosen a separate value to opt into dynamic.)
1 parent 2341019 commit 9673eeb

File tree

10 files changed

+141
-6
lines changed

10 files changed

+141
-6
lines changed

packages/next/src/client/app-dir/link.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ type InternalLinkProps = {
133133
* Prefetching is only enabled in production.
134134
*
135135
* - In the **App Router**:
136-
* - `null` (default): Prefetch behavior depends on static vs dynamic routes:
136+
* - `"auto"`, `null`, `undefined` (default): Prefetch behavior depends on static vs dynamic routes:
137137
* - Static routes: fully prefetched
138138
* - Dynamic routes: partial prefetch to the nearest segment with a `loading.js`
139139
* - `true`: Always prefetch the full route and data.
@@ -151,7 +151,7 @@ type InternalLinkProps = {
151151
* </Link>
152152
* ```
153153
*/
154-
prefetch?: boolean | null
154+
prefetch?: boolean | 'auto' | null
155155

156156
/**
157157
* (unstable) Switch to a dynamic prefetch on hover. Effectively the same as
@@ -366,7 +366,9 @@ export default function LinkComponent(
366366
* - 'unstable_dynamicOnHover': this starts in "auto" mode, but switches to "full" when the link is hovered
367367
*/
368368
const appPrefetchKind =
369-
prefetchProp === null ? PrefetchKind.AUTO : PrefetchKind.FULL
369+
prefetchProp === null || prefetchProp === 'auto'
370+
? PrefetchKind.AUTO
371+
: PrefetchKind.FULL
370372

371373
if (process.env.NODE_ENV !== 'production') {
372374
function createPropError(args: {

packages/next/src/client/form-shared.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ type InternalFormProps = {
1919
* Prefetch can be disabled by passing `prefetch={false}`. Prefetching is only enabled in production.
2020
*
2121
* Options:
22-
* - `null` (default): For statically generated pages, this will prefetch the full React Server Component data. For dynamic pages, this will prefetch up to the nearest route segment with a [`loading.js`](https://nextjs.org/docs/app/api-reference/file-conventions/loading) file. If there is no loading file, it will not fetch the full tree to avoid fetching too much data.
22+
* - "auto", null, undefined (default): For statically generated pages, this will prefetch the full React Server Component data. For dynamic pages, this will prefetch up to the nearest route segment with a [`loading.js`](https://nextjs.org/docs/app/api-reference/file-conventions/loading) file. If there is no loading file, it will not fetch the full tree to avoid fetching too much data.
2323
* - `false`: This will not prefetch any data.
2424
*
2525
* In pages dir, prefetching is not supported, and passing this prop will emit a warning.

packages/next/src/client/link.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ type InternalLinkProps = {
7373
* Prefetch can be disabled by passing `prefetch={false}`. Prefetching is only enabled in production.
7474
*
7575
* In App Router:
76-
* - `null` (default): For statically generated pages, this will prefetch the full React Server Component data. For dynamic pages, this will prefetch up to the nearest route segment with a [`loading.js`](https://nextjs.org/docs/app/api-reference/file-conventions/loading) file. If there is no loading file, it will not fetch the full tree to avoid fetching too much data.
76+
* - "auto", null, undefined (default): For statically generated pages, this will prefetch the full React Server Component data. For dynamic pages, this will prefetch up to the nearest route segment with a [`loading.js`](https://nextjs.org/docs/app/api-reference/file-conventions/loading) file. If there is no loading file, it will not fetch the full tree to avoid fetching too much data.
7777
* - `true`: This will prefetch the full React Server Component data for all route segments, regardless of whether they contain a segment with `loading.js`.
7878
* - `false`: This will not prefetch any data, even on hover.
7979
*
@@ -82,7 +82,7 @@ type InternalLinkProps = {
8282
* - `false`: Prefetching will not happen when entering the viewport, but will still happen on hover.
8383
* @defaultValue `true` (pages router) or `null` (app router)
8484
*/
85-
prefetch?: boolean | null
85+
prefetch?: boolean | 'auto' | null
8686
/**
8787
* The active locale is automatically prepended. `locale` allows for providing a different locale.
8888
* When `false` `href` has to include the locale as the default behavior is disabled.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Loading() {
2+
return <div id="loading-boundary">Loading...</div>
3+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { connection } from 'next/server'
2+
3+
export default async function Page() {
4+
await connection()
5+
return <div id="dynamic-content">Dynamic content</div>
6+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export default function RootLayout({
2+
children,
3+
}: {
4+
children: React.ReactNode
5+
}) {
6+
return (
7+
<html lang="en">
8+
<body>{children}</body>
9+
</html>
10+
)
11+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { LinkAccordion } from '../components/link-accordion'
2+
3+
export default function Page() {
4+
return (
5+
<>
6+
<p>
7+
This page is used to test that prefetch="auto" uses the default
8+
prefetching strategy (the same as if no prefetch prop is given).
9+
</p>
10+
11+
<LinkAccordion
12+
// @ts-expect-error: "auto" not yet part of public types
13+
prefetch="auto"
14+
href="/dynamic"
15+
>
16+
Dynamic page with loading boundary
17+
</LinkAccordion>
18+
</>
19+
)
20+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use client'
2+
3+
import Link from 'next/link'
4+
import { useState } from 'react'
5+
6+
export function LinkAccordion({ href, children }) {
7+
const [isVisible, setIsVisible] = useState(false)
8+
return (
9+
<>
10+
<input
11+
type="checkbox"
12+
checked={isVisible}
13+
onChange={() => setIsVisible(!isVisible)}
14+
data-link-accordion={href}
15+
/>
16+
{isVisible ? (
17+
<Link href={href}>{children}</Link>
18+
) : (
19+
`${children} (link is hidden)`
20+
)}
21+
</>
22+
)
23+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* @type {import('next').NextConfig}
3+
*/
4+
const nextConfig = {
5+
experimental: {
6+
ppr: true,
7+
dynamicIO: true,
8+
},
9+
}
10+
11+
module.exports = nextConfig
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { nextTestSetup } from 'e2e-utils'
2+
import type * as Playwright from 'playwright'
3+
import { createRouterAct } from '../router-act'
4+
5+
describe('<Link prefetch="auto">', () => {
6+
const { next, isNextDev, skipped } = nextTestSetup({
7+
files: __dirname,
8+
skipDeployment: true,
9+
})
10+
if (isNextDev || skipped) {
11+
it('disabled in development / deployment', () => {})
12+
return
13+
}
14+
15+
// NOTE: Since "auto" is just an alias for the default, I'm not bothering to
16+
// write tests for every default prefetching behavior; that's covered by a
17+
// bunch of other test suites. This is just a quick test to confirm that the
18+
// alias exists.
19+
20+
it('<Link prefetch="auto"> works the same as if prefetch were undefined or null', async () => {
21+
// Test that the link only prefetches the static part of the target page
22+
let page: Playwright.Page
23+
const browser = await next.browser('/', {
24+
beforePageLoad(p: Playwright.Page) {
25+
page = p
26+
},
27+
})
28+
const act = createRouterAct(page)
29+
30+
// Reveal the link to trigger a prefetch
31+
await act(async () => {
32+
const linkToggle = await browser.elementByCss(
33+
'input[data-link-accordion="/dynamic"]'
34+
)
35+
await linkToggle.click()
36+
}, [
37+
// Should prefetch the loading boundary
38+
{
39+
includes: 'Loading...',
40+
},
41+
// Should not prefetch the dynamic content
42+
{
43+
includes: 'Dynamic content',
44+
block: 'reject',
45+
},
46+
])
47+
48+
// Navigate to the page
49+
await act(
50+
async () => {
51+
await browser.elementByCss('a[href="/dynamic"]').click()
52+
},
53+
{
54+
// Now the dynamic content should be fetched
55+
includes: 'Dynamic content',
56+
}
57+
)
58+
})
59+
})

0 commit comments

Comments
 (0)