Skip to content

Commit 2eae7ca

Browse files
committed
use FetchStrategy to control prefetching behavior everywhere
1 parent d7cd6f6 commit 2eae7ca

File tree

10 files changed

+142
-52
lines changed

10 files changed

+142
-52
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: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@ 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+
type PrefetchTaskFetchStrategy,
10+
} from './segment-cache'
711
import { createCacheKey } from './segment-cache'
812
import {
913
type PrefetchTask,
@@ -22,7 +26,7 @@ type Element = LinkElement | HTMLFormElement
2226
// shape for both to prevent a polymorphic de-opt in the VM.
2327
type LinkOrFormInstanceShared = {
2428
router: AppRouterInstance
25-
kind: PrefetchKind.AUTO | PrefetchKind.FULL
29+
fetchStrategy: PrefetchTaskFetchStrategy
2630

2731
isVisible: boolean
2832

@@ -140,7 +144,7 @@ export function mountLinkInstance(
140144
element: LinkElement,
141145
href: string,
142146
router: AppRouterInstance,
143-
kind: PrefetchKind.AUTO | PrefetchKind.FULL,
147+
fetchStrategy: PrefetchTaskFetchStrategy,
144148
prefetchEnabled: boolean,
145149
setOptimisticLinkStatus: (status: { pending: boolean }) => void
146150
): LinkInstance {
@@ -149,7 +153,7 @@ export function mountLinkInstance(
149153
if (prefetchURL !== null) {
150154
const instance: PrefetchableLinkInstance = {
151155
router,
152-
kind,
156+
fetchStrategy,
153157
isVisible: false,
154158
prefetchTask: null,
155159
prefetchHref: prefetchURL.href,
@@ -165,7 +169,7 @@ export function mountLinkInstance(
165169
// track its optimistic state (i.e. useLinkStatus).
166170
const instance: NonPrefetchableLinkInstance = {
167171
router,
168-
kind,
172+
fetchStrategy,
169173
isVisible: false,
170174
prefetchTask: null,
171175
prefetchHref: null,
@@ -178,7 +182,7 @@ export function mountFormInstance(
178182
element: HTMLFormElement,
179183
href: string,
180184
router: AppRouterInstance,
181-
kind: PrefetchKind.AUTO | PrefetchKind.FULL
185+
fetchStrategy: PrefetchTaskFetchStrategy
182186
): void {
183187
const prefetchURL = coercePrefetchableUrl(href)
184188
if (prefetchURL === null) {
@@ -190,7 +194,7 @@ export function mountFormInstance(
190194
}
191195
const instance: FormInstance = {
192196
router,
193-
kind,
197+
fetchStrategy,
194198
isVisible: false,
195199
prefetchTask: null,
196200
prefetchHref: prefetchURL.href,
@@ -261,7 +265,7 @@ export function onNavigationIntent(
261265
unstable_upgradeToDynamicPrefetch
262266
) {
263267
// Switch to a full, dynamic prefetch
264-
instance.kind = PrefetchKind.FULL
268+
instance.fetchStrategy = FetchStrategy.Full
265269
}
266270
rescheduleLinkPrefetch(instance, PrefetchPriority.Intent)
267271
}
@@ -303,7 +307,7 @@ function rescheduleLinkPrefetch(
303307
instance.prefetchTask = scheduleSegmentPrefetchTask(
304308
cacheKey,
305309
treeAtTimeOfPrefetch,
306-
instance.kind === PrefetchKind.FULL,
310+
instance.fetchStrategy,
307311
priority,
308312
null
309313
)
@@ -313,7 +317,7 @@ function rescheduleLinkPrefetch(
313317
reschedulePrefetchTask(
314318
existingPrefetchTask,
315319
treeAtTimeOfPrefetch,
316-
instance.kind === PrefetchKind.FULL,
320+
instance.fetchStrategy,
317321
priority
318322
)
319323
}
@@ -347,7 +351,7 @@ export function pingVisibleLinks(
347351
instance.prefetchTask = scheduleSegmentPrefetchTask(
348352
cacheKey,
349353
tree,
350-
instance.kind === PrefetchKind.FULL,
354+
instance.fetchStrategy,
351355
PrefetchPriority.Default,
352356
null
353357
)
@@ -364,7 +368,7 @@ function prefetchWithOldCacheImplementation(instance: PrefetchableInstance) {
364368
// note that `appRouter.prefetch()` is currently sync,
365369
// so we have to wrap this call in an async function to be able to catch() errors below.
366370
return instance.router.prefetch(instance.prefetchHref, {
367-
kind: instance.kind,
371+
kind: convertFetchStrategyToPrefetchKind(instance.fetchStrategy),
368372
})
369373
}
370374

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

Lines changed: 3 additions & 7 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]
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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+
/**
11+
* A subset of fetch strategies used for prefetch tasks.
12+
* A prefetch task can't know if it should use `PPR` or `LoadingBoundary`
13+
* until we complete the initial tree prefetch request, so we use `PPR` to signal both cases
14+
* and adjust it based on the route when actually fetching.
15+
* */
16+
export type PrefetchTaskFetchStrategy = FetchStrategy.PPR | FetchStrategy.Full
17+
18+
export function convertFetchStrategyToPrefetchKind(
19+
fetchStrategy: FetchStrategy
20+
): PrefetchKind.AUTO | PrefetchKind.FULL {
21+
switch (fetchStrategy) {
22+
case FetchStrategy.LoadingBoundary:
23+
case FetchStrategy.PPR: {
24+
return PrefetchKind.AUTO
25+
}
26+
case FetchStrategy.Full: {
27+
return PrefetchKind.FULL
28+
}
29+
default: {
30+
fetchStrategy satisfies never
31+
throw new InvariantError(`Unexpected FetchStrategy: ${fetchStrategy}`)
32+
}
33+
}
34+
}
35+
36+
export function convertPrefetchKindToFetchStrategy(
37+
prefetchKind: PrefetchKind.AUTO | PrefetchKind.FULL
38+
): PrefetchTaskFetchStrategy {
39+
switch (prefetchKind) {
40+
case PrefetchKind.AUTO: {
41+
// We default to PPR. We'll discover whether or not the route supports it with the initial prefetch.
42+
return FetchStrategy.PPR
43+
}
44+
case PrefetchKind.FULL: {
45+
return FetchStrategy.Full
46+
}
47+
48+
default: {
49+
prefetchKind satisfies never
50+
throw new InvariantError(`Unexpected PrefetchKind: ${prefetchKind}`)
51+
}
52+
}
53+
}

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ 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 {
6+
PrefetchPriority,
7+
type PrefetchTaskFetchStrategy,
8+
} from '../segment-cache'
69

710
/**
811
* Entrypoint for prefetching a URL into the Segment Cache.
@@ -12,8 +15,8 @@ import { PrefetchPriority } from '../segment-cache'
1215
* Roughly corresponds to the current URL.
1316
* @param treeAtTimeOfPrefetch - The FlightRouterState at the time the prefetch
1417
* 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}>.
18+
* @param fetchStrategy - Whether to prefetch dynamic data, in addition to
19+
* static data. This is used by `<Link prefetch={true}>`.
1720
* @param onInvalidate - A callback that will be called when the prefetch cache
1821
* When called, it signals to the listener that the data associated with the
1922
* prefetch may have been invalidated from the cache. This is not a live
@@ -28,7 +31,7 @@ export function prefetch(
2831
href: string,
2932
nextUrl: string | null,
3033
treeAtTimeOfPrefetch: FlightRouterState,
31-
includeDynamicData: boolean,
34+
fetchStrategy: PrefetchTaskFetchStrategy,
3235
onInvalidate: null | (() => void)
3336
) {
3437
const url = createPrefetchURL(href)
@@ -40,7 +43,7 @@ export function prefetch(
4043
schedulePrefetchTask(
4144
cacheKey,
4245
treeAtTimeOfPrefetch,
43-
includeDynamicData,
46+
fetchStrategy,
4447
PrefetchPriority.Default,
4548
onInvalidate
4649
)

0 commit comments

Comments
 (0)