Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit ba57d16

Browse files
committed
[Cache Components] Fix caching in generateMetadata/generateViewport
When using `'use cache'` in `generateMetadata` or `generateViewport`, the caching did not work correctly, because we didn't apply the same special handling for the `params` and `searchParams` props that we do for layout and page components. This caused the following issues: - `generateMetadata`/`generateViewport` for a page without params, or with static params, yielded cache misses when resuming - `generateMetadata` for a page/layout with unused params, became dynamic when prerendering a fallback shell - `generateMetadata` for a page with used search params, produced a timeout error during prerendering - `generateViewport` for a page/layout with unused params, produced a build error (unless opting into fully dynamic rendering) Still to be done in a follow-up: Omit unused `params` from cache keys, and upgrade cache keys when they are used, to avoid unnecessary cache misses when resuming. closes NAR-321
1 parent 0f68f3e commit ba57d16

File tree

17 files changed

+671
-90
lines changed

17 files changed

+671
-90
lines changed

packages/next/src/lib/metadata/resolve-metadata.ts

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import type { ParsedUrlQuery } from 'querystring'
2222
import type { StaticMetadata } from './types/icons'
2323
import type { WorkStore } from '../../server/app-render/work-async-storage.external'
2424
import type { Params } from '../../server/request/params'
25+
import type { SearchParams } from '../../server/request/search-params'
2526

2627
// eslint-disable-next-line import/no-extraneous-dependencies
2728
import 'server-only'
@@ -56,6 +57,11 @@ import { ResolveMetadataSpan } from '../../server/lib/trace/constants'
5657
import { PAGE_SEGMENT_KEY } from '../../shared/lib/segment'
5758
import * as Log from '../../build/output/log'
5859
import { createServerParamsForMetadata } from '../../server/request/params'
60+
import { isUseCacheFunction } from '../client-and-server-references'
61+
import type {
62+
UseCacheLayoutProps,
63+
UseCachePageProps,
64+
} from '../../server/use-cache/use-cache-wrapper'
5965

6066
type StaticIcons = Pick<ResolvedIcons, 'icon' | 'apple'>
6167

@@ -85,13 +91,17 @@ type BuildState = {
8591
}
8692

8793
type LayoutProps = {
88-
params: { [key: string]: any }
94+
params: Promise<Params>
8995
}
96+
9097
type PageProps = {
91-
params: { [key: string]: any }
92-
searchParams: { [key: string]: any }
98+
params: Promise<Params>
99+
searchParams: Promise<SearchParams>
93100
}
94101

102+
type SegmentProps = LayoutProps | PageProps
103+
type UseCacheSegmentProps = UseCacheLayoutProps | UseCachePageProps
104+
95105
function isFavicon(icon: IconDescriptor | undefined): boolean {
96106
if (!icon) {
97107
return false
@@ -367,11 +377,13 @@ function mergeViewport({
367377

368378
function getDefinedViewport(
369379
mod: any,
370-
props: any,
380+
props: SegmentProps,
371381
tracingProps: { route: string }
372382
): Viewport | ViewportResolver | null {
373383
if (typeof mod.generateViewport === 'function') {
374384
const { route } = tracingProps
385+
const segmentProps = createSegmentProps(mod.generateViewport, props)
386+
375387
return (parent: ResolvingViewport) =>
376388
getTracer().trace(
377389
ResolveMetadataSpan.generateViewport,
@@ -381,19 +393,21 @@ function getDefinedViewport(
381393
'next.page': route,
382394
},
383395
},
384-
() => mod.generateViewport(props, parent)
396+
() => mod.generateViewport(segmentProps, parent)
385397
)
386398
}
387399
return mod.viewport || null
388400
}
389401

390402
function getDefinedMetadata(
391403
mod: any,
392-
props: any,
404+
props: SegmentProps,
393405
tracingProps: { route: string }
394406
): Metadata | MetadataResolver | null {
395407
if (typeof mod.generateMetadata === 'function') {
396408
const { route } = tracingProps
409+
const segmentProps = createSegmentProps(mod.generateMetadata, props)
410+
397411
return (parent: ResolvingMetadata) =>
398412
getTracer().trace(
399413
ResolveMetadataSpan.generateMetadata,
@@ -403,15 +417,31 @@ function getDefinedMetadata(
403417
'next.page': route,
404418
},
405419
},
406-
() => mod.generateMetadata(props, parent)
420+
() => mod.generateMetadata(segmentProps, parent)
407421
)
408422
}
409423
return mod.metadata || null
410424
}
411425

426+
/**
427+
* If `fn` is a `'use cache'` function, we add special markers to the props,
428+
* that the cache wrapper reads and removes, before passing the props to the
429+
* user function.
430+
*/
431+
function createSegmentProps(
432+
fn: Function,
433+
props: SegmentProps
434+
): SegmentProps | UseCacheSegmentProps {
435+
return isUseCacheFunction(fn)
436+
? 'searchParams' in props
437+
? { ...props, $$isPage: true }
438+
: { ...props, $$isLayout: true }
439+
: props
440+
}
441+
412442
async function collectStaticImagesFiles(
413443
metadata: AppDirModules['metadata'],
414-
props: any,
444+
props: SegmentProps,
415445
type: keyof NonNullable<AppDirModules['metadata']>
416446
) {
417447
if (!metadata?.[type]) return undefined
@@ -428,7 +458,7 @@ async function collectStaticImagesFiles(
428458

429459
async function resolveStaticMetadata(
430460
modules: AppDirModules,
431-
props: any
461+
props: SegmentProps
432462
): Promise<StaticMetadata> {
433463
const { metadata } = modules
434464
if (!metadata) return null
@@ -463,7 +493,7 @@ async function collectMetadata({
463493
tree: LoaderTree
464494
metadataItems: MetadataItems
465495
errorMetadataItem: MetadataItems[number]
466-
props: any
496+
props: SegmentProps
467497
route: string
468498
errorConvention?: MetadataErrorType
469499
}) {
@@ -514,7 +544,7 @@ async function collectViewport({
514544
tree: LoaderTree
515545
viewportItems: ViewportItems
516546
errorViewportItemRef: ErrorViewportItemRef
517-
props: any
547+
props: SegmentProps
518548
route: string
519549
errorConvention?: MetadataErrorType
520550
}) {
@@ -606,25 +636,14 @@ async function resolveMetadataItemsImpl(
606636
}
607637

608638
const params = createServerParamsForMetadata(currentParams, workStore)
609-
610-
let layerProps: LayoutProps | PageProps
611-
if (isPage) {
612-
layerProps = {
613-
params,
614-
searchParams,
615-
}
616-
} else {
617-
layerProps = {
618-
params,
619-
}
620-
}
639+
const props: SegmentProps = isPage ? { params, searchParams } : { params }
621640

622641
await collectMetadata({
623642
tree,
624643
metadataItems,
625644
errorMetadataItem,
626645
errorConvention,
627-
props: layerProps,
646+
props,
628647
route: currentTreePrefix
629648
// __PAGE__ shouldn't be shown in a route
630649
.filter((s) => s !== PAGE_SEGMENT_KEY)

packages/next/src/server/app-render/create-component-tree.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ import type { Params } from '../request/params'
2525
import { workUnitAsyncStorage } from './work-unit-async-storage.external'
2626
import { OUTLET_BOUNDARY_NAME } from '../../lib/framework/boundary-constants'
2727
import type {
28-
UseCacheLayoutComponentProps,
29-
UseCachePageComponentProps,
28+
UseCacheLayoutProps,
29+
UseCachePageProps,
3030
} from '../use-cache/use-cache-wrapper'
3131
import { DEFAULT_SEGMENT_KEY } from '../../shared/lib/segment'
3232
import {
@@ -793,14 +793,14 @@ async function createComponentTreeInternal(
793793
let searchParams = createServerSearchParamsForServerPage(query, workStore)
794794

795795
if (isUseCacheFunction(PageComponent)) {
796-
const UseCachePageComponent: React.ComponentType<UseCachePageComponentProps> =
796+
const UseCachePageComponent: React.ComponentType<UseCachePageProps> =
797797
PageComponent
798798

799799
pageElement = (
800800
<UseCachePageComponent
801801
params={params}
802802
searchParams={searchParams}
803-
$$isPageComponent
803+
$$isPage
804804
/>
805805
)
806806
} else {
@@ -951,14 +951,14 @@ async function createComponentTreeInternal(
951951
let serverSegment: React.ReactNode
952952

953953
if (isUseCacheFunction(SegmentComponent)) {
954-
const UseCacheLayoutComponent: React.ComponentType<UseCacheLayoutComponentProps> =
954+
const UseCacheLayoutComponent: React.ComponentType<UseCacheLayoutProps> =
955955
SegmentComponent
956956

957957
serverSegment = (
958958
<UseCacheLayoutComponent
959959
{...parallelRouteProps}
960960
params={params}
961-
$$isLayoutComponent
961+
$$isLayout
962962
/>
963963
)
964964
} else {

packages/next/src/server/use-cache/use-cache-wrapper.ts

Lines changed: 58 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -91,20 +91,20 @@ type CacheKeyParts =
9191
| [buildId: string, id: string, args: unknown[]]
9292
| [buildId: string, id: string, args: unknown[], hmrRefreshHash: string]
9393

94-
interface UseCacheInnerPageComponentProps {
94+
interface UseCachePageInnerProps {
9595
params: Promise<Params>
9696
searchParams?: Promise<SearchParams>
9797
}
9898

99-
export interface UseCachePageComponentProps {
99+
export interface UseCachePageProps {
100100
params: Promise<Params>
101101
searchParams: Promise<SearchParams>
102-
$$isPageComponent: true
102+
$$isPage: true
103103
}
104104

105-
export type UseCacheLayoutComponentProps = {
105+
export type UseCacheLayoutProps = {
106106
params: Promise<Params>
107-
$$isLayoutComponent: true
107+
$$isLayout: true
108108
} & {
109109
// The value type should be React.ReactNode. But such an index signature would
110110
// be incompatible with the other two props.
@@ -995,25 +995,26 @@ export function cache(
995995
}
996996
}
997997

998-
let isPageOrLayout = false
999-
1000-
// For page and layout components, the cache function is overwritten,
1001-
// which allows us to apply special handling for params and searchParams.
1002-
// For pages and layouts we're using the outer params prop, and not the
1003-
// inner one that was serialized/deserialized. While it's not generally
1004-
// true for "use cache" args, in the case of `params` the inner and outer
1005-
// object are essentially equivalent, so this is safe to do (including
1006-
// fallback params that are hanging promises). It allows us to avoid
1007-
// waiting for the timeout, when prerendering a fallback shell of a cached
1008-
// page or layout that awaits params.
1009-
if (isPageComponent(args)) {
1010-
isPageOrLayout = true
998+
let isPageOrLayoutSegmentFunction = false
999+
1000+
// For page and layout segment functions (i.e. the page/layout component,
1001+
// or generateMetadata/generateViewport), the cache function is
1002+
// overwritten, which allows us to apply special handling for params and
1003+
// searchParams. For pages and layouts we're using the outer params prop,
1004+
// and not the inner one that was serialized/deserialized. While it's not
1005+
// generally true for "use cache" args, in the case of `params` the inner
1006+
// and outer object are essentially equivalent, so this is safe to do
1007+
// (including fallback params that are hanging promises). It allows us to
1008+
// avoid waiting for the timeout, when prerendering a fallback shell of a
1009+
// cached page or layout that awaits params.
1010+
if (isPageSegmentFunction(args)) {
1011+
isPageOrLayoutSegmentFunction = true
10111012

10121013
const [{ params: outerParams, searchParams: outerSearchParams }] = args
10131014

1014-
const props: UseCacheInnerPageComponentProps = {
1015+
const props: UseCachePageInnerProps = {
10151016
params: outerParams,
1016-
// Omit searchParams and $$isPageComponent.
1017+
// Omit searchParams and $$isPage.
10171018
}
10181019

10191020
if (isPrivate) {
@@ -1028,7 +1029,7 @@ export function cache(
10281029
[name]: async ({
10291030
params: _innerParams,
10301031
searchParams: innerSearchParams,
1031-
}: UseCacheInnerPageComponentProps) =>
1032+
}: UseCachePageInnerProps) =>
10321033
originalFn.apply(null, [
10331034
{
10341035
params: outerParams,
@@ -1045,19 +1046,22 @@ export function cache(
10451046
},
10461047
]),
10471048
}[name] as (...args: unknown[]) => Promise<unknown>
1048-
} else if (isLayoutComponent(args)) {
1049-
isPageOrLayout = true
1050-
1051-
const [{ params: outerParams, $$isLayoutComponent, ...outerSlots }] =
1052-
args
1053-
// Overwrite the props to omit $$isLayoutComponent.
1049+
} else if (isLayoutSegmentFunction(args)) {
1050+
isPageOrLayoutSegmentFunction = true
1051+
1052+
const [{ params: outerParams, $$isLayout, ...outerSlots }] = args
1053+
// Overwrite the props to omit $$isLayout. Note that slots are only
1054+
// passed to the layout component (if any are defined), and not to
1055+
// generateMetadata nor generateViewport. For those functions,
1056+
// outerSlots/innerSlots is an empty object, which is fine because we're
1057+
// just spreading it into the props.
10541058
args = [{ params: outerParams, ...outerSlots }]
10551059

10561060
fn = {
10571061
[name]: async ({
10581062
params: _innerParams,
10591063
...innerSlots
1060-
}: Omit<UseCacheLayoutComponentProps, '$$isLayoutComponent'>) =>
1064+
}: Omit<UseCacheLayoutProps, '$$isLayout'>) =>
10611065
originalFn.apply(null, [{ params: outerParams, ...innerSlots }]),
10621066
}[name] as (...args: unknown[]) => Promise<unknown>
10631067
}
@@ -1120,14 +1124,14 @@ export function cache(
11201124
//
11211125
// fallthrough
11221126
case 'prerender':
1123-
if (!isPageOrLayout) {
1124-
// If the "use cache" function is not a page or a layout, we need to
1125-
// track dynamic access already when encoding the arguments. If
1126-
// params are passed explicitly into a "use cache" function (as
1127-
// opposed to receiving them automatically in a page or layout), we
1128-
// assume that the params are also accessed. This allows us to abort
1129-
// early, and treat the function as dynamic, instead of waiting for
1130-
// the timeout to be reached.
1127+
if (!isPageOrLayoutSegmentFunction) {
1128+
// If the "use cache" function is not a page or layout segment
1129+
// function, we need to track dynamic access already when encoding
1130+
// the arguments. If params are passed explicitly into a "use cache"
1131+
// function (as opposed to receiving them automatically in a page or
1132+
// layout), we assume that the params are also accessed. This allows
1133+
// us to abort early, and treat the function as dynamic, instead of
1134+
// waiting for the timeout to be reached.
11311135
const dynamicAccessAbortController = new AbortController()
11321136

11331137
encodedCacheKeyParts = await dynamicAccessAsyncStorage.run(
@@ -1600,37 +1604,31 @@ export function cache(
16001604
return React.cache(cachedFn)
16011605
}
16021606

1603-
function isPageComponent(
1604-
args: any[]
1605-
): args is [UseCachePageComponentProps, undefined] {
1606-
if (args.length !== 2) {
1607-
return false
1608-
}
1609-
1610-
const [props, ref] = args
1607+
/**
1608+
* Returns `true` if the `'use cache'` function is the page component itself,
1609+
* or `generateMetadata`/`generateViewport` in a page file.
1610+
*/
1611+
function isPageSegmentFunction(args: any[]): args is [UseCachePageProps] {
1612+
const [maybeProps] = args
16111613

16121614
return (
1613-
ref === undefined && // server components receive an undefined ref arg
1614-
props !== null &&
1615-
typeof props === 'object' &&
1616-
(props as UseCachePageComponentProps).$$isPageComponent
1615+
maybeProps !== null &&
1616+
typeof maybeProps === 'object' &&
1617+
(maybeProps as UseCachePageProps).$$isPage === true
16171618
)
16181619
}
16191620

1620-
function isLayoutComponent(
1621-
args: any[]
1622-
): args is [UseCacheLayoutComponentProps, undefined] {
1623-
if (args.length !== 2) {
1624-
return false
1625-
}
1626-
1627-
const [props, ref] = args
1621+
/**
1622+
* Returns `true` if the `'use cache'` function is the layout component itself,
1623+
* or `generateMetadata`/`generateViewport` in a layout file.
1624+
*/
1625+
function isLayoutSegmentFunction(args: any[]): args is [UseCacheLayoutProps] {
1626+
const [maybeProps] = args
16281627

16291628
return (
1630-
ref === undefined && // server components receive an undefined ref arg
1631-
props !== null &&
1632-
typeof props === 'object' &&
1633-
(props as UseCacheLayoutComponentProps).$$isLayoutComponent
1629+
maybeProps !== null &&
1630+
typeof maybeProps === 'object' &&
1631+
(maybeProps as UseCacheLayoutProps).$$isLayout === true
16341632
)
16351633
}
16361634

0 commit comments

Comments
 (0)