Skip to content

Commit 8ae4310

Browse files
committed
use FetchStrategy to control prefetching behavior everywhere
1 parent e9eddf0 commit 8ae4310

File tree

10 files changed

+143
-55
lines changed

10 files changed

+143
-55
lines changed

packages/next/errors.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -755,5 +755,7 @@
755755
"754": "%s cannot be used outside of a request context.",
756756
"755": "Route must be a string",
757757
"756": "Route %s not found",
758-
"757": "Unknown styled string type: %s"
758+
"757": "Unknown styled string type: %s",
759+
"758": "Unexpected FetchStrategy: %s",
760+
"759": "Unexpected PrefetchKind: %s"
759761
}

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
AppRouterContext,
88
type AppRouterInstance,
99
} from '../../shared/lib/app-router-context.shared-runtime'
10-
import { PrefetchKind } from '../components/router-reducer/router-reducer-types'
1110
import {
1211
checkFormActionUrl,
1312
createFormSubmitDestinationUrl,
@@ -20,6 +19,7 @@ import {
2019
mountFormInstance,
2120
unmountPrefetchableInstance,
2221
} from '../components/links'
22+
import { FetchStrategy } from '../components/segment-cache'
2323

2424
export type { FormProps }
2525

@@ -99,7 +99,13 @@ export default function Form({
9999
const observeFormVisibilityOnMount = useCallback(
100100
(element: HTMLFormElement) => {
101101
if (isPrefetchEnabled && router !== null) {
102-
mountFormInstance(element, actionProp, router, PrefetchKind.AUTO)
102+
mountFormInstance(
103+
element,
104+
actionProp,
105+
router,
106+
// We default to PPR. We'll discover whether or not the route supports it with the initial prefetch.
107+
FetchStrategy.PPR
108+
)
103109
}
104110
return () => {
105111
unmountPrefetchableInstance(element)

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import React, { createContext, useContext, useOptimistic, useRef } from 'react'
44
import type { UrlObject } from 'url'
55
import { formatUrl } from '../../shared/lib/router/utils/format-url'
66
import { AppRouterContext } from '../../shared/lib/app-router-context.shared-runtime'
7-
import { PrefetchKind } from '../components/router-reducer/router-reducer-types'
87
import { useMergedRef } from '../use-merged-ref'
98
import { isAbsoluteUrl } from '../../shared/lib/utils'
109
import { addBasePath } from '../add-base-path'
@@ -21,6 +20,7 @@ import {
2120
import { isLocalURL } from '../../shared/lib/router/utils/is-local-url'
2221
import { dispatchNavigateAction } from '../components/app-router-instance'
2322
import { errorOnce } from '../../shared/lib/utils/error-once'
23+
import { FetchStrategy } from '../components/segment-cache'
2424

2525
type Url = string | UrlObject
2626
type RequiredKeys<T> = {
@@ -363,10 +363,11 @@ export default function LinkComponent(
363363
* - false: we will not prefetch if in the viewport at all
364364
* - 'unstable_dynamicOnHover': this starts in "auto" mode, but switches to "full" when the link is hovered
365365
*/
366-
const appPrefetchKind =
366+
const fetchStrategy =
367367
prefetchProp === null || prefetchProp === 'auto'
368-
? PrefetchKind.AUTO
369-
: PrefetchKind.FULL
368+
? // We default to PPR. We'll discover whether or not the route supports it with the initial prefetch.
369+
FetchStrategy.PPR
370+
: FetchStrategy.Full
370371

371372
if (process.env.NODE_ENV !== 'production') {
372373
function createPropError(args: {
@@ -581,7 +582,7 @@ export default function LinkComponent(
581582
element,
582583
href,
583584
router,
584-
appPrefetchKind,
585+
fetchStrategy,
585586
prefetchEnabled,
586587
setOptimisticLinkStatus
587588
)
@@ -595,7 +596,7 @@ export default function LinkComponent(
595596
unmountPrefetchableInstance(element)
596597
}
597598
},
598-
[prefetchEnabled, href, router, appPrefetchKind, setOptimisticLinkStatus]
599+
[prefetchEnabled, href, router, fetchStrategy, setOptimisticLinkStatus]
599600
)
600601

601602
const mergedRef = useMergedRef(observeLinkVisibilityOnMount, childRef)

packages/next/src/client/components/app-router-instance.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ import {
1414
import { reducer } from './router-reducer/router-reducer'
1515
import { startTransition } from 'react'
1616
import { isThenable } from '../../shared/lib/is-thenable'
17-
import { prefetch as prefetchWithSegmentCache } from './segment-cache'
17+
import {
18+
convertPrefetchKindToFetchStrategy,
19+
prefetch as prefetchWithSegmentCache,
20+
} from './segment-cache'
1821
import { dispatchAppRouterAction } from './use-action-queue'
1922
import { addBasePath } from '../add-base-path'
2023
import { createPrefetchURL, isExternalURL } from './app-router'
@@ -323,11 +326,16 @@ export const publicAppRouterInstance: AppRouterInstance = {
323326
// cache. So we don't need to dispatch an action.
324327
(href: string, options?: PrefetchOptions) => {
325328
const actionQueue = getAppRouterActionQueue()
329+
const prefetchKind = options?.kind ?? PrefetchKind.AUTO
330+
if (prefetchKind === PrefetchKind.TEMPORARY) {
331+
// This concept doesn't exist in the segment cache implementation.
332+
return
333+
}
326334
prefetchWithSegmentCache(
327335
href,
328336
actionQueue.state.nextUrl,
329337
actionQueue.state.tree,
330-
options?.kind === PrefetchKind.FULL,
338+
convertPrefetchKindToFetchStrategy(prefetchKind),
331339
options?.onInvalidate ?? null
332340
)
333341
}

packages/next/src/client/components/links.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ import type { FlightRouterState } from '../../server/app-render/types'
22
import type { AppRouterInstance } from '../../shared/lib/app-router-context.shared-runtime'
33
import { getCurrentAppRouterState } from './app-router-instance'
44
import { createPrefetchURL } from './app-router'
5-
import { PrefetchKind } from './router-reducer/router-reducer-types'
6-
import { isPrefetchTaskDirty } from './segment-cache'
5+
import {
6+
convertFetchStrategyToPrefetchKind,
7+
FetchStrategy,
8+
isPrefetchTaskDirty,
9+
} from './segment-cache'
710
import { createCacheKey } from './segment-cache'
811
import {
912
type PrefetchTask,
@@ -22,7 +25,7 @@ type Element = LinkElement | HTMLFormElement
2225
// shape for both to prevent a polymorphic de-opt in the VM.
2326
type LinkOrFormInstanceShared = {
2427
router: AppRouterInstance
25-
kind: PrefetchKind.AUTO | PrefetchKind.FULL
28+
fetchStrategy: FetchStrategy
2629

2730
isVisible: boolean
2831

@@ -140,7 +143,7 @@ export function mountLinkInstance(
140143
element: LinkElement,
141144
href: string,
142145
router: AppRouterInstance,
143-
kind: PrefetchKind.AUTO | PrefetchKind.FULL,
146+
fetchStrategy: FetchStrategy,
144147
prefetchEnabled: boolean,
145148
setOptimisticLinkStatus: (status: { pending: boolean }) => void
146149
): LinkInstance {
@@ -149,7 +152,7 @@ export function mountLinkInstance(
149152
if (prefetchURL !== null) {
150153
const instance: PrefetchableLinkInstance = {
151154
router,
152-
kind,
155+
fetchStrategy,
153156
isVisible: false,
154157
prefetchTask: null,
155158
prefetchHref: prefetchURL.href,
@@ -165,7 +168,7 @@ export function mountLinkInstance(
165168
// track its optimistic state (i.e. useLinkStatus).
166169
const instance: NonPrefetchableLinkInstance = {
167170
router,
168-
kind,
171+
fetchStrategy,
169172
isVisible: false,
170173
prefetchTask: null,
171174
prefetchHref: null,
@@ -178,7 +181,7 @@ export function mountFormInstance(
178181
element: HTMLFormElement,
179182
href: string,
180183
router: AppRouterInstance,
181-
kind: PrefetchKind.AUTO | PrefetchKind.FULL
184+
fetchStrategy: FetchStrategy
182185
): void {
183186
const prefetchURL = coercePrefetchableUrl(href)
184187
if (prefetchURL === null) {
@@ -190,7 +193,7 @@ export function mountFormInstance(
190193
}
191194
const instance: FormInstance = {
192195
router,
193-
kind,
196+
fetchStrategy,
194197
isVisible: false,
195198
prefetchTask: null,
196199
prefetchHref: prefetchURL.href,
@@ -261,7 +264,7 @@ export function onNavigationIntent(
261264
unstable_upgradeToDynamicPrefetch
262265
) {
263266
// Switch to a full, dynamic prefetch
264-
instance.kind = PrefetchKind.FULL
267+
instance.fetchStrategy = FetchStrategy.Full
265268
}
266269
rescheduleLinkPrefetch(instance, PrefetchPriority.Intent)
267270
}
@@ -303,7 +306,7 @@ function rescheduleLinkPrefetch(
303306
instance.prefetchTask = scheduleSegmentPrefetchTask(
304307
cacheKey,
305308
treeAtTimeOfPrefetch,
306-
instance.kind === PrefetchKind.FULL,
309+
instance.fetchStrategy,
307310
priority,
308311
null
309312
)
@@ -313,7 +316,7 @@ function rescheduleLinkPrefetch(
313316
reschedulePrefetchTask(
314317
existingPrefetchTask,
315318
treeAtTimeOfPrefetch,
316-
instance.kind === PrefetchKind.FULL,
319+
instance.fetchStrategy,
317320
priority
318321
)
319322
}
@@ -347,7 +350,7 @@ export function pingVisibleLinks(
347350
instance.prefetchTask = scheduleSegmentPrefetchTask(
348351
cacheKey,
349352
tree,
350-
instance.kind === PrefetchKind.FULL,
353+
instance.fetchStrategy,
351354
PrefetchPriority.Default,
352355
null
353356
)
@@ -364,7 +367,7 @@ function prefetchWithOldCacheImplementation(instance: PrefetchableInstance) {
364367
// note that `appRouter.prefetch()` is currently sync,
365368
// so we have to wrap this call in an async function to be able to catch() errors below.
366369
return instance.router.prefetch(instance.prefetchHref, {
367-
kind: instance.kind,
370+
kind: convertFetchStrategyToPrefetchKind(instance.fetchStrategy),
368371
})
369372
}
370373

packages/next/src/client/components/segment-cache-impl/cache.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import {
6363
DOC_PREFETCH_RANGE_HEADER_VALUE,
6464
doesExportedHtmlMatchBuildId,
6565
} from '../../../shared/lib/segment-cache/output-export-prefetch-encoding'
66+
import { FetchStrategy } from '../segment-cache'
6667

6768
// A note on async/await when working in the prefetch cache:
6869
//
@@ -165,12 +166,6 @@ export type RouteCacheEntry =
165166
| FulfilledRouteCacheEntry
166167
| RejectedRouteCacheEntry
167168

168-
export const enum FetchStrategy {
169-
PPR,
170-
Full,
171-
LoadingBoundary,
172-
}
173-
174169
type SegmentCacheEntryShared = {
175170
staleAt: number
176171
fetchStrategy: FetchStrategy
@@ -415,7 +410,8 @@ export function getSegmentKeypathForTask(
415410
// If we're fetching using PPR, we do not need to include the search params in
416411
// the cache key, because the search params are treated as dynamic data. The
417412
// cache entry is valid for all possible search param values.
418-
const isDynamicTask = task.includeDynamicData || !route.isPPREnabled
413+
const isDynamicTask =
414+
task.fetchStrategy === FetchStrategy.Full || !route.isPPREnabled
419415
return isDynamicTask && path.endsWith('/' + PAGE_SEGMENT_KEY)
420416
? [path, route.renderedSearch]
421417
: [path]
@@ -1161,8 +1157,15 @@ export async function fetchRouteOnCacheMiss(
11611157
routeIsPPREnabled
11621158
)
11631159
} else {
1164-
// PPR is not enabled for this route. The server responds with a
1165-
// different format (FlightRouterState) that we need to convert.
1160+
// PPR is not enabled for this route.
1161+
1162+
// We start "auto" prefetches with a PPR strategy by default. If it's that (and not e.g. a `FetchStrategy.Full`),
1163+
// we should update the task accordingly.
1164+
if (task.fetchStrategy === FetchStrategy.PPR) {
1165+
task.fetchStrategy = FetchStrategy.LoadingBoundary
1166+
}
1167+
1168+
// For non-PPR routes, the server responds with a different format (FlightRouterState) that we need to convert.
11661169
// TODO: We will unify the responses eventually. I'm keeping the types
11671170
// separate for now because FlightRouterState has so many
11681171
// overloaded concerns.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { InvariantError } from '../../../shared/lib/invariant-error'
2+
import { PrefetchKind } from '../router-reducer/router-reducer-types'
3+
4+
export const enum FetchStrategy {
5+
PPR,
6+
Full,
7+
LoadingBoundary,
8+
}
9+
10+
export function convertFetchStrategyToPrefetchKind(
11+
fetchStrategy: FetchStrategy
12+
): PrefetchKind.AUTO | PrefetchKind.FULL {
13+
switch (fetchStrategy) {
14+
case FetchStrategy.LoadingBoundary:
15+
case FetchStrategy.PPR: {
16+
return PrefetchKind.AUTO
17+
}
18+
case FetchStrategy.Full: {
19+
return PrefetchKind.FULL
20+
}
21+
default: {
22+
fetchStrategy satisfies never
23+
throw new InvariantError(`Unexpected FetchStrategy: ${fetchStrategy}`)
24+
}
25+
}
26+
}
27+
28+
export function convertPrefetchKindToFetchStrategy(
29+
prefetchKind: PrefetchKind.AUTO | PrefetchKind.FULL
30+
) {
31+
switch (prefetchKind) {
32+
case PrefetchKind.AUTO: {
33+
// We default to PPR. We'll discover whether or not the route supports it with the initial prefetch.
34+
return FetchStrategy.PPR
35+
}
36+
case PrefetchKind.FULL: {
37+
return FetchStrategy.Full
38+
}
39+
40+
default: {
41+
prefetchKind satisfies never
42+
throw new InvariantError(`Unexpected PrefetchKind: ${prefetchKind}`)
43+
}
44+
}
45+
}

packages/next/src/client/components/segment-cache-impl/prefetch.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { FlightRouterState } from '../../../server/app-render/types'
22
import { createPrefetchURL } from '../app-router'
33
import { createCacheKey } from './cache-key'
44
import { schedulePrefetchTask } from './scheduler'
5-
import { PrefetchPriority } from '../segment-cache'
5+
import { type FetchStrategy, PrefetchPriority } from '../segment-cache'
66

77
/**
88
* Entrypoint for prefetching a URL into the Segment Cache.
@@ -12,8 +12,8 @@ import { PrefetchPriority } from '../segment-cache'
1212
* Roughly corresponds to the current URL.
1313
* @param treeAtTimeOfPrefetch - The FlightRouterState at the time the prefetch
1414
* was requested. This is only used when PPR is disabled.
15-
* @param includeDynamicData - Whether to prefetch dynamic data, in addition to
16-
* static data. This is used by <Link prefetch={true}>.
15+
* @param fetchStrategy - Whether to prefetch dynamic data, in addition to
16+
* static data. This is used by `<Link prefetch={true}>`.
1717
* @param onInvalidate - A callback that will be called when the prefetch cache
1818
* When called, it signals to the listener that the data associated with the
1919
* prefetch may have been invalidated from the cache. This is not a live
@@ -28,7 +28,7 @@ export function prefetch(
2828
href: string,
2929
nextUrl: string | null,
3030
treeAtTimeOfPrefetch: FlightRouterState,
31-
includeDynamicData: boolean,
31+
fetchStrategy: FetchStrategy,
3232
onInvalidate: null | (() => void)
3333
) {
3434
const url = createPrefetchURL(href)
@@ -40,7 +40,7 @@ export function prefetch(
4040
schedulePrefetchTask(
4141
cacheKey,
4242
treeAtTimeOfPrefetch,
43-
includeDynamicData,
43+
fetchStrategy,
4444
PrefetchPriority.Default,
4545
onInvalidate
4646
)

0 commit comments

Comments
 (0)