Skip to content

Commit 616f6fa

Browse files
authored
[backport][metadata] render streaming metadata on the top level (#80566)
1 parent 3ab8db7 commit 616f6fa

File tree

11 files changed

+40
-113
lines changed

11 files changed

+40
-113
lines changed

packages/next/src/client/components/router-reducer/reducers/find-head-in-cache.ts

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,27 +22,16 @@ function findHeadInCacheImpl(
2222

2323
// First try the 'children' parallel route if it exists
2424
// when starting from the "root", this corresponds with the main page component
25-
if (parallelRoutes.children) {
26-
const [segment, childParallelRoutes] = parallelRoutes.children
27-
const childSegmentMap = cache.parallelRoutes.get('children')
28-
if (childSegmentMap) {
29-
const cacheKey = createRouterCacheKey(segment)
30-
const cacheNode = childSegmentMap.get(cacheKey)
31-
if (cacheNode) {
32-
const item = findHeadInCacheImpl(
33-
cacheNode,
34-
childParallelRoutes,
35-
keyPrefix + '/' + cacheKey
36-
)
37-
if (item) return item
38-
}
39-
}
40-
}
25+
const parallelRoutesKeys = Object.keys(parallelRoutes).filter(
26+
(key) => key !== 'children'
27+
)
4128

42-
// if we didn't find metadata in the page slot, check the other parallel routes
43-
for (const key in parallelRoutes) {
44-
if (key === 'children') continue // already checked above
29+
// if we are at the root, we need to check the children slot first
30+
if ('children' in parallelRoutes) {
31+
parallelRoutesKeys.unshift('children')
32+
}
4533

34+
for (const key of parallelRoutesKeys) {
4635
const [segment, childParallelRoutes] = parallelRoutes[key]
4736
const childSegmentMap = cache.parallelRoutes.get(key)
4837
if (!childSegmentMap) {

packages/next/src/lib/metadata/metadata.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,9 +206,11 @@ export function createMetadataComponents({
206206
const promise = resolveFinalMetadata()
207207
if (serveStreamingMetadata) {
208208
return (
209-
<Suspense fallback={null}>
210-
<AsyncMetadata promise={promise} />
211-
</Suspense>
209+
<div hidden>
210+
<Suspense fallback={null}>
211+
<AsyncMetadata promise={promise} />
212+
</Suspense>
213+
</div>
212214
)
213215
}
214216
const metadataState = await promise

packages/next/src/server/app-render/app-render.tsx

Lines changed: 13 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,8 @@ interface ParseRequestHeadersOptions {
242242
}
243243

244244
const flightDataPathHeadKey = 'h'
245+
const getFlightViewportKey = (requestId: string) => requestId + 'v'
246+
const getFlightMetadataKey = (requestId: string) => requestId + 'm'
245247

246248
interface ParsedRequestHeaders {
247249
/**
@@ -333,30 +335,6 @@ function createNotFoundLoaderTree(loaderTree: LoaderTree): LoaderTree {
333335
]
334336
}
335337

336-
function createDivergedMetadataComponents(
337-
Metadata: React.ComponentType,
338-
serveStreamingMetadata: boolean
339-
): {
340-
StaticMetadata: React.ComponentType<{}>
341-
StreamingMetadata: React.ComponentType<{}> | null
342-
} {
343-
function EmptyMetadata() {
344-
return null
345-
}
346-
const StreamingMetadata: React.ComponentType | null = serveStreamingMetadata
347-
? Metadata
348-
: null
349-
350-
const StaticMetadata: React.ComponentType<{}> = serveStreamingMetadata
351-
? EmptyMetadata
352-
: Metadata
353-
354-
return {
355-
StaticMetadata,
356-
StreamingMetadata,
357-
}
358-
}
359-
360338
/**
361339
* Returns a function that parses the dynamic segment and return the associated value.
362340
*/
@@ -524,14 +502,6 @@ async function generateDynamicRSCPayload(
524502
serveStreamingMetadata,
525503
})
526504

527-
const { StreamingMetadata, StaticMetadata } =
528-
createDivergedMetadataComponents(() => {
529-
return (
530-
// Adding requestId as react key to make metadata remount for each render
531-
<MetadataTree key={requestId} />
532-
)
533-
}, serveStreamingMetadata)
534-
535505
flightData = (
536506
await walkTreeWithFlightRouterState({
537507
ctx,
@@ -548,9 +518,9 @@ async function generateDynamicRSCPayload(
548518
isPossibleServerAction={ctx.isPossibleServerAction}
549519
/>
550520
{/* Adding requestId as react key to make metadata remount for each render */}
551-
<ViewportTree key={requestId} />
552-
{StreamingMetadata ? <StreamingMetadata /> : null}
553-
<StaticMetadata />
521+
<ViewportTree key={getFlightViewportKey(requestId)} />
522+
{/* Not add requestId as react key to ensure segment prefetch could result consistently if nothing changed */}
523+
<MetadataTree key={getFlightMetadataKey(requestId)} />
554524
</React.Fragment>
555525
),
556526
injectedCSS: new Set(),
@@ -854,14 +824,6 @@ async function getRSCPayload(
854824

855825
const preloadCallbacks: PreloadCallbacks = []
856826

857-
const { StreamingMetadata, StaticMetadata } =
858-
createDivergedMetadataComponents(() => {
859-
return (
860-
// Not add requestId as react key to ensure segment prefetch could result consistently if nothing changed
861-
<MetadataTree />
862-
)
863-
}, serveStreamingMetadata)
864-
865827
const seedData = await createComponentTree({
866828
ctx,
867829
loaderTree: tree,
@@ -875,7 +837,6 @@ async function getRSCPayload(
875837
missingSlots,
876838
preloadCallbacks,
877839
authInterrupts: ctx.renderOpts.experimental.authInterrupts,
878-
StreamingMetadata,
879840
StreamingMetadataOutlet,
880841
})
881842

@@ -893,8 +854,9 @@ async function getRSCPayload(
893854
statusCode={ctx.res.statusCode}
894855
isPossibleServerAction={ctx.isPossibleServerAction}
895856
/>
896-
<ViewportTree key={ctx.requestId} />
897-
<StaticMetadata />
857+
<ViewportTree key={getFlightViewportKey(ctx.requestId)} />
858+
{/* Not add requestId as react key to ensure segment prefetch could result consistently if nothing changed */}
859+
<MetadataTree />
898860
</React.Fragment>
899861
)
900862

@@ -981,16 +943,8 @@ async function getErrorRSCPayload(
981943
serveStreamingMetadata: serveStreamingMetadata,
982944
})
983945

984-
const { StreamingMetadata, StaticMetadata } =
985-
createDivergedMetadataComponents(
986-
() => (
987-
<React.Fragment key={flightDataPathHeadKey}>
988-
{/* Adding requestId as react key to make metadata remount for each render */}
989-
<MetadataTree key={requestId} />
990-
</React.Fragment>
991-
),
992-
serveStreamingMetadata
993-
)
946+
// {/* Adding requestId as react key to make metadata remount for each render */}
947+
const metadata = <MetadataTree key={getFlightMetadataKey(requestId)} />
994948

995949
const initialHead = (
996950
<React.Fragment key={flightDataPathHeadKey}>
@@ -1000,12 +954,11 @@ async function getErrorRSCPayload(
1000954
isPossibleServerAction={ctx.isPossibleServerAction}
1001955
/>
1002956
{/* Adding requestId as react key to make metadata remount for each render */}
1003-
<ViewportTree key={requestId} />
957+
<ViewportTree key={getFlightViewportKey(requestId)} />
1004958
{process.env.NODE_ENV === 'development' && (
1005959
<meta name="next-error" content="not-found" />
1006960
)}
1007-
{StreamingMetadata ? <StreamingMetadata /> : null}
1008-
<StaticMetadata />
961+
{metadata}
1009962
</React.Fragment>
1010963
)
1011964

@@ -1025,10 +978,7 @@ async function getErrorRSCPayload(
1025978
const seedData: CacheNodeSeedData = [
1026979
initialTree[0],
1027980
<html id="__next_error__">
1028-
<head>
1029-
{StreamingMetadata ? <StreamingMetadata /> : null}
1030-
<StaticMetadata />
1031-
</head>
981+
<head>{metadata}</head>
1032982
<body>
1033983
{process.env.NODE_ENV !== 'production' && err ? (
1034984
<template

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

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ export function createComponentTree(props: {
3939
missingSlots?: Set<string>
4040
preloadCallbacks: PreloadCallbacks
4141
authInterrupts: boolean
42-
StreamingMetadata: React.ComponentType | null
4342
StreamingMetadataOutlet: React.ComponentType
4443
}): Promise<CacheNodeSeedData> {
4544
return getTracer().trace(
@@ -76,7 +75,6 @@ async function createComponentTreeInternal({
7675
missingSlots,
7776
preloadCallbacks,
7877
authInterrupts,
79-
StreamingMetadata,
8078
StreamingMetadataOutlet,
8179
}: {
8280
loaderTree: LoaderTree
@@ -91,7 +89,6 @@ async function createComponentTreeInternal({
9189
missingSlots?: Set<string>
9290
preloadCallbacks: PreloadCallbacks
9391
authInterrupts: boolean
94-
StreamingMetadata: React.ComponentType | null
9592
StreamingMetadataOutlet: React.ComponentType | null
9693
}): Promise<CacheNodeSeedData> {
9794
const {
@@ -388,7 +385,6 @@ async function createComponentTreeInternal({
388385

389386
// Resolve the segment param
390387
const actualSegment = segmentParam ? segmentParam.treeSegment : segment
391-
const metadata = StreamingMetadata ? <StreamingMetadata /> : undefined
392388

393389
// Use the same condition to render metadataOutlet as metadata
394390
const metadataOutlet = StreamingMetadataOutlet ? (
@@ -511,7 +507,6 @@ async function createComponentTreeInternal({
511507
missingSlots,
512508
preloadCallbacks,
513509
authInterrupts,
514-
StreamingMetadata: isChildrenRouteKey ? StreamingMetadata : null,
515510
// `StreamingMetadataOutlet` is used to conditionally throw. In the case of parallel routes we will have more than one page
516511
// but we only want to throw on the first one.
517512
StreamingMetadataOutlet: isChildrenRouteKey
@@ -694,12 +689,6 @@ async function createComponentTreeInternal({
694689
actualSegment,
695690
<React.Fragment key={cacheNodeKey}>
696691
{pageElement}
697-
{/*
698-
* The order here matters since a parent might call findDOMNode().
699-
* findDOMNode() will return the first child if multiple children are rendered.
700-
* But React will hoist metadata into <head> which breaks scroll handling.
701-
*/}
702-
{metadata}
703692
{layerAssets}
704693
<OutletBoundary>
705694
<MetadataOutlet ready={getViewportReady} />

packages/next/src/server/app-render/walk-tree-with-flight-router-state.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,6 @@ export async function walkTreeWithFlightRouterState({
202202
getMetadataReady,
203203
preloadCallbacks,
204204
authInterrupts: experimental.authInterrupts,
205-
StreamingMetadata: null,
206205
StreamingMetadataOutlet,
207206
}
208207
)

test/e2e/esm-externals/app/client/page.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import World3 from 'app-cjs-esm-package/entry'
66

77
export default function Index() {
88
return (
9-
<div>
9+
<p>
1010
Hello {World1}+{World2}+{World3}
11-
</div>
11+
</p>
1212
)
1313
}

test/e2e/esm-externals/app/server/page.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import World3 from 'app-cjs-esm-package/entry'
44

55
export default function Page() {
66
return (
7-
<div>
7+
<p>
88
Hello {World1}+{World2}+{World3}
9-
</div>
9+
</p>
1010
)
1111
}

test/e2e/esm-externals/esm-externals.test.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,13 @@ describe('esm-externals', () => {
2828

2929
it('should return the correct SSR HTML', async () => {
3030
const $ = await next.render$(url)
31-
const body = $('body > div > div').html()
31+
const body = $('body p').html()
3232
expect(normalize(body)).toEqual(expectedHtml)
3333
})
3434

3535
it('should render the correct page', async () => {
3636
const browser = await next.browser(url)
37-
expect(await browser.elementByCss('body > div').text()).toEqual(
38-
expectedText
39-
)
37+
expect(await browser.elementByCss('body p').text()).toEqual(expectedText)
4038
})
4139
})
4240

@@ -54,13 +52,13 @@ describe('esm-externals', () => {
5452

5553
it('should return the correct SSR HTML', async () => {
5654
const $ = await next.render$(url)
57-
const body = $('body > div').html()
55+
const body = $('body > p').html()
5856
expect(normalize(body)).toEqual(expectedHtml)
5957
})
6058

6159
it('should render the correct page', async () => {
6260
const browser = await next.browser(url)
63-
expect(await browser.elementByCss('body > div').text()).toEqual(
61+
expect(await browser.elementByCss('body > p').text()).toEqual(
6462
expectedText
6563
)
6664
})

test/e2e/esm-externals/pages/ssg.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ export async function getStaticProps() {
1313

1414
export default function Index({ worlds }) {
1515
return (
16-
<div>
16+
<p>
1717
Hello {World1}+{World2}+{World3}+{worlds}
18-
</div>
18+
</p>
1919
)
2020
}

test/e2e/esm-externals/pages/ssr.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ export function getServerSideProps() {
1313

1414
export default function Index({ worlds }) {
1515
return (
16-
<div>
16+
<p>
1717
Hello {World1}+{World2}+{World3}+{worlds}
18-
</div>
18+
</p>
1919
)
2020
}

test/e2e/esm-externals/pages/static.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ const worlds = 'World+World+World'
77

88
export default function Index() {
99
return (
10-
<div>
10+
<p>
1111
Hello {World1}+{World2}+{World3}+{worlds}
12-
</div>
12+
</p>
1313
)
1414
}

0 commit comments

Comments
 (0)