Skip to content

Commit

Permalink
Update root not found boundary to not introduce Server Component (#70328
Browse files Browse the repository at this point in the history
)

The server component is going to be updated to be passed to a client
component in a future update which is not possible. This change
relocates where the root not found boundary is added to avoid creating
an intermediate server component when the layout is a client component
  • Loading branch information
gnoff authored Sep 23, 2024
1 parent a09ba05 commit c8c15e0
Showing 1 changed file with 60 additions and 58 deletions.
118 changes: 60 additions & 58 deletions packages/next/src/server/app-render/create-component-tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -276,52 +276,13 @@ async function createComponentTreeInternal({
/**
* The React Component to render.
*/
let Component = LayoutOrPage
const parallelKeys = Object.keys(parallelRoutes)
const hasSlotKey = parallelKeys.length > 1

// TODO-APP: This is a hack to support unmatched parallel routes, which will throw `notFound()`.
// This ensures that a `NotFoundBoundary` is available for when that happens,
// but it's not ideal, as it needlessly invokes the `NotFound` component and renders the `RootLayout` twice.
// We should instead look into handling the fallback behavior differently in development mode so that it doesn't
// rely on the `NotFound` behavior.
if (hasSlotKey && rootLayoutAtThisLevel && LayoutOrPage) {
Component = (componentProps: { params: Params }) => {
const NotFoundComponent = NotFound
const RootLayoutComponent = LayoutOrPage
return (
<NotFoundBoundary
notFound={
NotFoundComponent ? (
<Segment
isDynamicIO={experimental.dynamicIO}
isStaticGeneration={isStaticGeneration}
ready={getMetadataReady}
>
{layerAssets}
{/*
* We are intentionally only forwarding params to the root layout, as passing any of the parallel route props
* might trigger `notFound()`, which is not currently supported in the root layout.
*/}
<RootLayoutComponent params={componentProps.params}>
{notFoundStyles}
<NotFoundComponent />
</RootLayoutComponent>
</Segment>
) : undefined
}
>
<RootLayoutComponent {...componentProps} />
</NotFoundBoundary>
)
}
}
let MaybeComponent = LayoutOrPage

if (process.env.NODE_ENV === 'development') {
const { isValidElementType } = require('next/dist/compiled/react-is')
if (
(isPage || typeof Component !== 'undefined') &&
!isValidElementType(Component)
(isPage || typeof MaybeComponent !== 'undefined') &&
!isValidElementType(MaybeComponent)
) {
errorMissingDefaultExport(pagePath, 'page')
}
Expand Down Expand Up @@ -387,7 +348,7 @@ async function createComponentTreeInternal({
// prefetch everything up to the first route segment that defines a
// loading.tsx boundary. (We do the same if there's no loading
// boundary in the entire tree, because we don't want to prefetch too
// much) The rest of the tree is defered until the actual navigation.
// much) The rest of the tree is deferred until the actual navigation.
// It does not take into account whether the data is dynamic — even if
// the tree is completely static, it will still defer everything
// inside the loading boundary.
Expand Down Expand Up @@ -496,7 +457,7 @@ async function createComponentTreeInternal({
: null

// 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) {
if (!MaybeComponent) {
return [
actualSegment,
<Segment
Expand All @@ -513,6 +474,8 @@ async function createComponentTreeInternal({
]
}

const Component = MaybeComponent

// If force-dynamic is used and the current render supports postponing, we
// replace it with a node that will postpone the render. This ensures that the
// postpone is invoked during the react render phase and not during the next
Expand Down Expand Up @@ -602,23 +565,62 @@ async function createComponentTreeInternal({
} else {
props.params = createDynamicallyTrackedParams(currentParams)

const isRootLayoutWithChildrenSlotAndAtLeastOneMoreSlot =
rootLayoutAtThisLevel &&
'children' in parallelRoutes &&
Object.keys(parallelRoutes).length > 1

let serverSegment = <Component {...props} />

let segmentNode: React.ReactNode
if (isRootLayoutWithChildrenSlotAndAtLeastOneMoreSlot) {
// TODO-APP: This is a hack to support unmatched parallel routes, which will throw `notFound()`.
// This ensures that a `NotFoundBoundary` is available for when that happens,
// but it's not ideal, as it needlessly invokes the `NotFound` component and renders the `RootLayout` twice.
// We should instead look into handling the fallback behavior differently in development mode so that it doesn't
// rely on the `NotFound` behavior.
segmentNode = (
<Segment
isDynamicIO={experimental.dynamicIO}
isStaticGeneration={isStaticGeneration}
ready={getMetadataReady}
>
<NotFoundBoundary
notFound={
NotFound ? (
<>
{layerAssets}
<Component params={props.params}>
{notFoundStyles}
<NotFound />
</Component>
</>
) : undefined
}
>
{layerAssets}
{serverSegment}
</NotFoundBoundary>
</Segment>
)
} else {
segmentNode = (
<Segment
key={cacheNodeKey}
isDynamicIO={experimental.dynamicIO}
isStaticGeneration={isStaticGeneration}
ready={getMetadataReady}
>
{layerAssets}
{serverSegment}
</Segment>
)
}

// For layouts we just render the component
return [
actualSegment,
// It is critical that this tree render something other than `null` because the client router uses
// null to represent an lazy hole. The current implementation satisfies this because the two inner slots
// ensure there is a fragment even if both slots render null. If we ever refactor this to only render the component
// or similar we need to ensure there is a fragment. Long term we should move to using a Symbol to communicate
// a lazy hole rather than null
<Segment
key={cacheNodeKey}
isDynamicIO={experimental.dynamicIO}
isStaticGeneration={isStaticGeneration}
ready={getMetadataReady}
>
{layerAssets}
<Component {...props} />
</Segment>,
segmentNode,
parallelRouteCacheNodeSeedData,
loadingData,
]
Expand Down

0 comments on commit c8c15e0

Please sign in to comment.