Skip to content

Commit

Permalink
move layerAssets into CacheNodeSeedData (#67436)
Browse files Browse the repository at this point in the history
Rather than adding a new property to `FlightDataPath` (which is already
a bit messy as-is, given all the array index access / slicing), this
combines the `layerAssets` value (which are essentially styles and
script tags) into the `ReactNode` included as part of
`CacheNodeSeedData`.

This means that anywhere we render `cache.rsc`, we'll also be rendering
the styles/scripts associated, which was the intended behavior by the
first PR that was reverted in this stack.

`createComponentTree` now only returns the `CacheNodeSeedData` type,
simplifying the usage a bit.
  • Loading branch information
ztanner authored Jul 9, 2024
1 parent 301f414 commit c96fa41
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 124 deletions.
3 changes: 0 additions & 3 deletions packages/next/src/client/components/layout-router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,6 @@ export default function OuterLayoutRouter({
template,
notFound,
notFoundStyles,
styles,
}: {
parallelRouterKey: string
segmentPath: FlightSegmentPath
Expand All @@ -509,7 +508,6 @@ export default function OuterLayoutRouter({
template: React.ReactNode
notFound: React.ReactNode | undefined
notFoundStyles: React.ReactNode | undefined
styles?: React.ReactNode
}) {
const context = useContext(LayoutRouterContext)
if (!context) {
Expand Down Expand Up @@ -542,7 +540,6 @@ export default function OuterLayoutRouter({

return (
<>
{styles}
{preservedSegments.map((preservedSegment) => {
const preservedSegmentValue = getSegmentValue(preservedSegment)
const cacheKey = createRouterCacheKey(preservedSegment)
Expand Down
47 changes: 22 additions & 25 deletions packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,7 @@ async function ReactServerApp({ tree, ctx, asNotFound }: ReactServerAppProps) {
createDynamicallyTrackedSearchParams,
})

const { seedData, styles } = await createComponentTree({
const seedData = await createComponentTree({
ctx,
createSegmentPath: (child) => child,
loaderTree: tree,
Expand All @@ -498,30 +498,27 @@ async function ReactServerApp({ tree, ctx, asNotFound }: ReactServerAppProps) {
typeof varyHeader === 'string' && varyHeader.includes(NEXT_URL)

return (
<>
{styles}
<AppRouter
buildId={ctx.renderOpts.buildId}
assetPrefix={ctx.assetPrefix}
initialCanonicalUrl={url.pathname + url.search}
// This is the router state tree.
initialTree={initialTree}
// This is the tree of React nodes that are seeded into the cache
initialSeedData={seedData}
couldBeIntercepted={couldBeIntercepted}
initialHead={
<>
<NonIndex ctx={ctx} />
{/* Adding requestId as react key to make metadata remount for each render */}
<MetadataTree key={ctx.requestId} />
</>
}
globalErrorComponent={GlobalError}
// This is used to provide debug information (when in development mode)
// about which slots were not filled by page components while creating the component tree.
missingSlots={missingSlots}
/>
</>
<AppRouter
buildId={ctx.renderOpts.buildId}
assetPrefix={ctx.assetPrefix}
initialCanonicalUrl={url.pathname + url.search}
// This is the router state tree.
initialTree={initialTree}
// This is the tree of React nodes that are seeded into the cache
initialSeedData={seedData}
couldBeIntercepted={couldBeIntercepted}
initialHead={
<>
<NonIndex ctx={ctx} />
{/* Adding requestId as react key to make metadata remount for each render */}
<MetadataTree key={ctx.requestId} />
</>
}
globalErrorComponent={GlobalError}
// This is used to provide debug information (when in development mode)
// about which slots were not filled by page components while creating the component tree.
missingSlots={missingSlots}
/>
)
}

Expand Down
131 changes: 63 additions & 68 deletions packages/next/src/server/app-render/create-component-tree.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { FlightSegmentPath, CacheNodeSeedData } from './types'
import React, { type ReactNode } from 'react'
import React from 'react'
import { isClientReference } from '../../lib/client-reference'
import { getLayoutOrPageModule } from '../lib/app-dir-module'
import type { LoaderTree } from '../lib/app-dir-module'
Expand All @@ -16,11 +16,6 @@ import { NextNodeServerSpan } from '../lib/trace/constants'
import { StaticGenBailoutError } from '../../client/components/static-generation-bailout'
import type { LoadingModuleData } from '../../shared/lib/app-router-context.shared-runtime'

type ComponentTree = {
seedData: CacheNodeSeedData
styles: ReactNode
}

type Params = {
[key: string]: string | string[]
}
Expand All @@ -41,7 +36,7 @@ export function createComponentTree(props: {
metadataOutlet?: React.ReactNode
ctx: AppRenderContext
missingSlots?: Set<string>
}): Promise<ComponentTree> {
}): Promise<CacheNodeSeedData> {
return getTracer().trace(
NextNodeServerSpan.createComponentTree,
{
Expand Down Expand Up @@ -83,7 +78,7 @@ async function createComponentTreeInternal({
metadataOutlet?: React.ReactNode
ctx: AppRenderContext
missingSlots?: Set<string>
}): Promise<ComponentTree> {
}): Promise<CacheNodeSeedData> {
const {
renderOpts: { nextConfigOutput, experimental },
staticGenerationStore,
Expand Down Expand Up @@ -374,7 +369,6 @@ async function createComponentTreeInternal({
// if we're prefetching and that there's a Loading component, we bail out
// otherwise we keep rendering for the prefetch.
// We also want to bail out if there's no Loading component in the tree.
let currentStyles = undefined
let childCacheNodeSeedData: CacheNodeSeedData | null = null

if (
Expand Down Expand Up @@ -426,27 +420,25 @@ async function createComponentTreeInternal({
}
}

const { seedData, styles: childComponentStyles } =
await createComponentTreeInternal({
createSegmentPath: (child) => {
return createSegmentPath([...currentSegmentPath, ...child])
},
loaderTree: parallelRoute,
parentParams: currentParams,
rootLayoutIncluded: rootLayoutIncludedAtThisLevelOrAbove,
injectedCSS: injectedCSSWithCurrentLayout,
injectedJS: injectedJSWithCurrentLayout,
injectedFontPreloadTags: injectedFontPreloadTagsWithCurrentLayout,
asNotFound,
// The metadataOutlet is responsible for throwing any errors that were caught during metadata resolution.
// We only want to render an outlet once per segment, as otherwise the error will be triggered
// multiple times causing an uncaught error.
metadataOutlet: isChildrenRouteKey ? metadataOutlet : undefined,
ctx,
missingSlots,
})

currentStyles = childComponentStyles
const seedData = await createComponentTreeInternal({
createSegmentPath: (child) => {
return createSegmentPath([...currentSegmentPath, ...child])
},
loaderTree: parallelRoute,
parentParams: currentParams,
rootLayoutIncluded: rootLayoutIncludedAtThisLevelOrAbove,
injectedCSS: injectedCSSWithCurrentLayout,
injectedJS: injectedJSWithCurrentLayout,
injectedFontPreloadTags: injectedFontPreloadTagsWithCurrentLayout,
asNotFound,
// The metadataOutlet is responsible for throwing any errors that were caught during metadata resolution.
// We only want to render an outlet once per segment, as otherwise the error will be triggered
// multiple times causing an uncaught error.
metadataOutlet: isChildrenRouteKey ? metadataOutlet : undefined,
ctx,
missingSlots,
})

childCacheNodeSeedData = seedData
}

Expand All @@ -469,7 +461,6 @@ async function createComponentTreeInternal({
templateScripts={templateScripts}
notFound={notFoundComponent}
notFoundStyles={notFoundStyles}
styles={currentStyles}
/>,
childCacheNodeSeedData,
]
Expand All @@ -494,20 +485,20 @@ async function createComponentTreeInternal({

// When the segment does not have a layout or page we still have to add the layout router to ensure the path holds the loading component
if (!Component) {
return {
seedData: [
actualSegment,
parallelRouteCacheNodeSeedData,
// TODO: I don't think the extra fragment is necessary. React treats top
// level fragments as transparent, i.e. the runtime behavior should be
// identical even without it. But maybe there's some findDOMNode-related
// reason that I'm not aware of, so I'm leaving it as-is out of extreme
// caution, for now.
<>{parallelRouteProps.children}</>,
loadingData,
],
styles: layerAssets,
}
return [
actualSegment,
parallelRouteCacheNodeSeedData,
// TODO: I don't think the extra fragment is necessary. React treats top
// level fragments as transparent, i.e. the runtime behavior should be
// identical even without it. But maybe there's some findDOMNode-related
// reason that I'm not aware of, so I'm leaving it as-is out of extreme
// caution, for now.
<>
{layerAssets}
{parallelRouteProps.children}
</>,
loadingData,
]
}

// If force-dynamic is used and the current render supports postponing, we
Expand All @@ -525,19 +516,19 @@ async function createComponentTreeInternal({
staticGenerationStore.forceDynamic &&
staticGenerationStore.prerenderState
) {
return {
seedData: [
actualSegment,
parallelRouteCacheNodeSeedData,
return [
actualSegment,
parallelRouteCacheNodeSeedData,
<>
<Postpone
prerenderState={staticGenerationStore.prerenderState}
reason='dynamic = "force-dynamic" was used'
route={staticGenerationStore.route}
/>,
loadingData,
],
styles: layerAssets,
}
/>
{layerAssets}
</>,
loadingData,
]
}

const isClientComponent = isClientReference(layoutOrPageMod)
Expand Down Expand Up @@ -592,6 +583,7 @@ async function createComponentTreeInternal({
<>
{metadataOutlet}
<ClientPageRoot props={props} Component={Component} />
{layerAssets}
</>
)
} else {
Expand All @@ -602,32 +594,35 @@ async function createComponentTreeInternal({
<>
{metadataOutlet}
<Component {...props} />
{layerAssets}
</>
)
}
} else {
// For layouts we just render the component
segmentElement = <Component {...props} />
segmentElement = (
<>
{layerAssets}
<Component {...props} />
</>
)
}

return {
seedData: [
actualSegment,
parallelRouteCacheNodeSeedData,
<>
{segmentElement}
{/* This null is currently critical. The wrapped Component can render null and if there was not fragment
return [
actualSegment,
parallelRouteCacheNodeSeedData,
<>
{segmentElement}
{/* This null is currently critical. The wrapped Component can render null and if there was not fragment
surrounding it this would look like a pending tree data state on the client which will cause an error
and break the app. Long-term we need to move away from using null as a partial tree identifier since it
is a valid return type for the components we wrap. Once we make this change we can safely remove the
fragment. The reason the extra null here is required is that fragments which only have 1 child are elided.
If the Component above renders null the actual tree data will look like `[null, null]`. If we remove the extra
null it will look like `null` (the array is elided) and this is what confuses the client router.
TODO-APP update router to use a Symbol for partial tree detection */}
{null}
</>,
loadingData,
],
styles: layerAssets,
}
{null}
</>,
loadingData,
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type {
FlightSegmentPath,
Segment,
} from './types'
import React from 'react'
import {
canSegmentBeOverridden,
matchSegment,
Expand All @@ -16,9 +15,7 @@ import {
addSearchParamsIfPageSegment,
createFlightRouterStateFromLoaderTree,
} from './create-flight-router-state-from-loader-tree'
import { parseLoaderTree } from './parse-loader-tree'
import type { CreateSegmentPath, AppRenderContext } from './app-render'
import { getLayerAssets } from './get-layer-assets'
import { hasLoadingComponentInTree } from './has-loading-component-in-tree'
import { createComponentTree } from './create-component-tree'
import { DEFAULT_SEGMENT_KEY } from '../../shared/lib/segment'
Expand Down Expand Up @@ -140,7 +137,7 @@ export async function walkTreeWithFlightRouterState({
return [[overriddenSegment, routerState, null, null]]
} else {
// Create component tree using the slice of the loaderTree
const { seedData } = await createComponentTree(
const seedData = await createComponentTree(
// This ensures flightRouterPath is valid and filters down the tree
{
ctx,
Expand All @@ -158,23 +155,7 @@ export async function walkTreeWithFlightRouterState({
}
)

// Create head
const { layoutOrPagePath } = parseLoaderTree(loaderTreeToFilter)
const layerAssets = getLayerAssets({
ctx,
layoutOrPagePath,
injectedCSS: new Set(injectedCSS),
injectedJS: new Set(injectedJS),
injectedFontPreloadTags: new Set(injectedFontPreloadTags),
})
const head = (
<>
{layerAssets}
{rscPayloadHead}
</>
)

return [[overriddenSegment, routerState, seedData, head]]
return [[overriddenSegment, routerState, seedData, rscPayloadHead]]
}
}

Expand Down
Loading

0 comments on commit c96fa41

Please sign in to comment.