diff --git a/packages/next/src/client/components/app-router.tsx b/packages/next/src/client/components/app-router.tsx index dabfb3b7a066b..1bc1eb4a2e2e5 100644 --- a/packages/next/src/client/components/app-router.tsx +++ b/packages/next/src/client/components/app-router.tsx @@ -54,6 +54,8 @@ import type { FlightRouterState } from '../../server/app-render/types' import { useNavFailureHandler } from './nav-failure-handler' import { useServerActionDispatcher } from '../app-call-server' import type { AppRouterActionQueue } from '../../shared/lib/router/action-queue' +import { prefetch as prefetchWithSegmentCache } from '../components/segment-cache/prefetch' + import { getRedirectTypeFromError, getURLFromRedirectError, @@ -69,6 +71,43 @@ function isExternalURL(url: URL) { return url.origin !== window.location.origin } +/** + * Given a link href, constructs the URL that should be prefetched. Returns null + * in cases where prefetching should be disabled, like external URLs, or + * during development. + * @param href The href passed to , router.prefetch(), or similar + * @returns A URL object to prefetch, or null if prefetching should be disabled + */ +export function createPrefetchURL(href: string): URL | null { + // Don't prefetch for bots as they don't navigate. + if (isBot(window.navigator.userAgent)) { + return null + } + + let url: URL + try { + url = new URL(addBasePath(href), window.location.href) + } catch (_) { + // TODO: Does this need to throw or can we just console.error instead? Does + // anyone rely on this throwing? (Seems unlikely.) + throw new Error( + `Cannot prefetch '${href}' because it cannot be converted to a URL.` + ) + } + + // Don't prefetch during development (improves compilation performance) + if (process.env.NODE_ENV === 'development') { + return null + } + + // External urls can't be prefetched in the same way. + if (isExternalURL(url)) { + return null + } + + return url +} + function HistoryUpdater({ appRouterState, }: { @@ -241,38 +280,25 @@ function Router({ const routerInstance: AppRouterInstance = { back: () => window.history.back(), forward: () => window.history.forward(), - prefetch: (href, options) => { - // Don't prefetch for bots as they don't navigate. - if (isBot(window.navigator.userAgent)) { - return - } - - let url: URL - try { - url = new URL(addBasePath(href), window.location.href) - } catch (_) { - throw new Error( - `Cannot prefetch '${href}' because it cannot be converted to a URL.` - ) - } - - // Don't prefetch during development (improves compilation performance) - if (process.env.NODE_ENV === 'development') { - return - } - - // External urls can't be prefetched in the same way. - if (isExternalURL(url)) { - return - } - startTransition(() => { - dispatch({ - type: ACTION_PREFETCH, - url, - kind: options?.kind ?? PrefetchKind.FULL, - }) - }) - }, + prefetch: + process.env.__NEXT_PPR && process.env.__NEXT_CLIENT_SEGMENT_CACHE + ? // Unlike the old implementation, the Segment Cache doesn't store its + // data in the router reducer state; it writes into a global mutable + // cache. So we don't need to dispatch an action. + prefetchWithSegmentCache + : (href, options) => { + // Use the old prefetch implementation. + const url = createPrefetchURL(href) + if (url !== null) { + startTransition(() => { + dispatch({ + type: ACTION_PREFETCH, + url, + kind: options?.kind ?? PrefetchKind.FULL, + }) + }) + } + }, replace: (href, options = {}) => { startTransition(() => { navigate(href, 'replace', options.scroll ?? true) diff --git a/packages/next/src/client/components/router-reducer/reducers/prefetch-reducer.ts b/packages/next/src/client/components/router-reducer/reducers/prefetch-reducer.ts index ed05a573c454d..8edd75bbfb3bf 100644 --- a/packages/next/src/client/components/router-reducer/reducers/prefetch-reducer.ts +++ b/packages/next/src/client/components/router-reducer/reducers/prefetch-reducer.ts @@ -3,7 +3,6 @@ import type { ReducerState, ReadonlyReducerState, } from '../router-reducer-types' -import { NEXT_RSC_UNION_QUERY } from '../../app-router-headers' import { PromiseQueue } from '../../promise-queue' import { getOrCreatePrefetchCacheEntry, @@ -12,7 +11,22 @@ import { export const prefetchQueue = new PromiseQueue(5) -export function prefetchReducer( +export const prefetchReducer = + process.env.__NEXT_PPR && process.env.__NEXT_CLIENT_SEGMENT_CACHE + ? identityReducerWhenSegmentCacheIsEnabled + : prefetchReducerImpl + +function identityReducerWhenSegmentCacheIsEnabled(state: T): T { + // Unlike the old implementation, the Segment Cache doesn't store its data in + // the router reducer state. + // + // This shouldn't be reachable because we wrap the prefetch API in a check, + // too, which prevents the action from being dispatched. But it's here for + // clarity + code elimination. + return state +} + +function prefetchReducerImpl( state: ReadonlyReducerState, action: PrefetchAction ): ReducerState { @@ -20,7 +34,6 @@ export function prefetchReducer( prunePrefetchCache(state.prefetchCache) const { url } = action - url.searchParams.delete(NEXT_RSC_UNION_QUERY) getOrCreatePrefetchCacheEntry({ url, diff --git a/packages/next/src/client/components/segment-cache/prefetch.ts b/packages/next/src/client/components/segment-cache/prefetch.ts new file mode 100644 index 0000000000000..384c905c33493 --- /dev/null +++ b/packages/next/src/client/components/segment-cache/prefetch.ts @@ -0,0 +1,16 @@ +import { createPrefetchURL } from '../../components/app-router' + +/** + * Entrypoint for prefetching a URL into the Segment Cache. + * @param href - The URL to prefetch. Typically this will come from a , + * or router.prefetch. It must be validated before we attempt to prefetch it. + */ +export function prefetch(href: string) { + const url = createPrefetchURL(href) + if (url === null) { + // This href should not be prefetched. + return + } + + // TODO: Not yet implemented +}