Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/next/src/client/components/app-router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,9 @@ function Router({
parentCacheNode: cache,
parentSegmentPath: null,
parentParams: {},
// This is the <Activity> "name" that shows up in the Suspense DevTools.
// It represents the root of the app.
debugNameContext: '/',
// Root node always has `url`
// Provided in AppTreeContext to ensure it can be overwritten in layout-router
url: canonicalUrl,
Expand Down
94 changes: 50 additions & 44 deletions packages/next/src/client/components/layout-router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { LoadingModuleData } from '../../shared/lib/app-router-types'
import type {
FlightRouterState,
FlightSegmentPath,
Segment,
} from '../../shared/lib/app-router-types'
import type { ErrorComponent } from './error-boundary'
import {
Expand All @@ -23,6 +24,7 @@ import React, {
Suspense,
useDeferredValue,
type JSX,
type ActivityProps,
} from 'react'
import ReactDOM from 'react-dom'
import {
Expand Down Expand Up @@ -338,13 +340,15 @@ function ScrollAndFocusHandler({
function InnerLayoutRouter({
tree,
segmentPath,
debugNameContext,
cacheNode,
params,
url,
isActive,
}: {
tree: FlightRouterState
segmentPath: FlightSegmentPath
debugNameContext: ActivityProps['name']
cacheNode: CacheNode
params: Params
url: string
Expand Down Expand Up @@ -469,6 +473,7 @@ function InnerLayoutRouter({
parentCacheNode: cacheNode,
parentSegmentPath: segmentPath,
parentParams: params,
debugNameContext: debugNameContext,

// TODO-APP: overriding of url for parallel routes
url: url,
Expand All @@ -487,9 +492,11 @@ function InnerLayoutRouter({
* If no loading property is provided it renders the children without a suspense boundary.
*/
function LoadingBoundary({
name,
loading,
children,
}: {
name: ActivityProps['name']
loading: LoadingModuleData | Promise<LoadingModuleData>
children: React.ReactNode
}): JSX.Element {
Expand Down Expand Up @@ -519,7 +526,7 @@ function LoadingBoundary({
const loadingScripts = loadingModuleData[2]
return (
<Suspense
name="loading.tsx"
name={name}
fallback={
<>
{loadingStyles}
Expand Down Expand Up @@ -577,6 +584,7 @@ export default function OuterLayoutRouter({
parentParams,
url,
isActive,
debugNameContext: parentDebugNameContext,
} = context

// Get the CacheNode for this segment by reading it from the parent segment's
Expand Down Expand Up @@ -696,6 +704,12 @@ export default function OuterLayoutRouter({
}
}

const debugName = getBoundaryDebugNameFromSegment(segment)
// `debugNameContext` represents the nearest non-"virtual" parent segment.
// `getBoundaryDebugNameFromSegment` returns null for virtual segments.
// So if `debugName` is null, the context is passed through unchanged.
const debugNameContext = debugName ?? parentDebugNameContext

// TODO: The loading module data for a segment is stored on the parent, then
// applied to each of that parent segment's parallel route slots. In the
// simple case where there's only one parallel route (the `children` slot),
Expand All @@ -715,7 +729,7 @@ export default function OuterLayoutRouter({
errorStyles={errorStyles}
errorScripts={errorScripts}
>
<LoadingBoundary loading={loadingModuleData}>
<LoadingBoundary name={debugName} loading={loadingModuleData}>
<HTTPAccessFallbackBoundary
notFound={notFound}
forbidden={forbidden}
Expand All @@ -728,6 +742,7 @@ export default function OuterLayoutRouter({
params={params}
cacheNode={cacheNode}
segmentPath={segmentPath}
debugNameContext={debugNameContext}
isActive={isActive && stateKey === activeStateKey}
/>
{segmentBoundaryTriggerNode}
Expand Down Expand Up @@ -758,10 +773,17 @@ export default function OuterLayoutRouter({
}

if (process.env.__NEXT_CACHE_COMPONENTS) {
const boundaryName = getBoundaryNameForSuspenseDevTools(tree)
child = (
<Activity
name={boundaryName}
// In practical terms, clicking this name in the Suspense DevTools
// should select the child slots of that layout.
//
// So the name we apply to the Activity boundary is actually based on
// the nearest parent segments.
//
// We skip over "virtual" parents, i.e. ones inserted by Next.js that
// don't correspond to application-defined code.
name={debugNameContext}
key={stateKey}
mode={stateKey === activeStateKey ? 'visible' : 'hidden'}
>
Expand All @@ -778,49 +800,33 @@ export default function OuterLayoutRouter({
return children
}

function getBoundaryNameForSuspenseDevTools(
subtree: FlightRouterState
): string | undefined {
const segment = subtree[0]

function getBoundaryDebugNameFromSegment(segment: Segment): string | undefined {
if (segment === '/') {
// Reached the root
return '/'
}
if (typeof segment === 'string') {
const children = subtree[1]
const isPage = Object.keys(children).length === 0
if (isPage) {
// Page segment
return '/'
}

// Layout segment

// Skip over "virtual" layouts that don't correspond to app-
// defined components.
if (
segment === '' ||
// For some reason, the loader tree sometimes includes extra __PAGE__
// "layouts" when part of a parallel route. But it's not a leaf node.
// Otherwise, we wouldn't need this special case because pages are
// always leaf nodes.
// TODO: Investigate why the loader produces these fake page segments.
segment.startsWith(PAGE_SEGMENT_KEY) ||
// This is inserted by the loader. We should consider encoding these
// in a more special way instead of checking the name, to distinguish them
// from app-defined groups.
segment[0] === '(virtual)'
) {
if (isVirtualLayout(segment)) {
return undefined
} else {
return segment + '/'
}

if (segment === '/_not-found') {
// Special case. For some reason, the name itself already has a
// leading slash.
return '/_not-found/'
}

return `/${segment}/`
}

// Parameterized segments are always layouts
const paramCacheKey = segment[1]
return `/${paramCacheKey}/`
return paramCacheKey + '/'
}

function isVirtualLayout(segment: string): boolean {
return (
// This is inserted by the loader. We should consider encoding these
// in a more special way instead of checking the name, to distinguish them
// from app-defined groups.
segment === '(slot)' ||
// For some reason, the loader tree sometimes includes extra __PAGE__
// "layouts" when part of a parallel route. But it's not a leaf node.
// Otherwise, we wouldn't need this special case because pages are
// always leaf nodes.
// TODO: Investigate why the loader produces these fake page segments.
segment.startsWith(PAGE_SEGMENT_KEY)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export const LayoutRouterContext = React.createContext<{
parentCacheNode: CacheNode
parentSegmentPath: FlightSegmentPath | null
parentParams: Params
debugNameContext: string | undefined
url: string
isActive: boolean
} | null>(null)
Expand Down
30 changes: 15 additions & 15 deletions test/development/acceptance-app/hydration-error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe('Error overlay for hydration errors in App router', () => {
<ScrollAndFocusHandler segmentPath={[...]}>
<InnerScrollAndFocusHandler segmentPath={[...]} focusAndScrollRef={{apply:false, ...}}>
<ErrorBoundary errorComponent={undefined} errorStyles={undefined} errorScripts={undefined}>
<LoadingBoundary loading={null}>
<LoadingBoundary name={undefined} loading={null}>
<HTTPAccessFallbackBoundary notFound={undefined} forbidden={undefined} unauthorized={undefined}>
<RedirectBoundary>
<RedirectErrorBoundary router={{...}}>
Expand Down Expand Up @@ -107,7 +107,7 @@ describe('Error overlay for hydration errors in App router', () => {
<ScrollAndFocusHandler segmentPath={[...]}>
<InnerScrollAndFocusHandler segmentPath={[...]} focusAndScrollRef={{apply:false, ...}}>
<ErrorBoundary errorComponent={undefined} errorStyles={undefined} errorScripts={undefined}>
<LoadingBoundary loading={null}>
<LoadingBoundary name={undefined} loading={null}>
<HTTPAccessFallbackBoundary notFound={undefined} forbidden={undefined} unauthorized={undefined}>
<RedirectBoundary>
<RedirectErrorBoundary router={{...}}>
Expand Down Expand Up @@ -181,7 +181,7 @@ describe('Error overlay for hydration errors in App router', () => {
<ScrollAndFocusHandler segmentPath={[...]}>
<InnerScrollAndFocusHandler segmentPath={[...]} focusAndScrollRef={{apply:false, ...}}>
<ErrorBoundary errorComponent={undefined} errorStyles={undefined} errorScripts={undefined}>
<LoadingBoundary loading={null}>
<LoadingBoundary name="(extra-att..." loading={null}>
<HTTPAccessFallbackBoundary notFound={<SegmentViewNode>} forbidden={undefined} unauthorized={undefined}>
<HTTPAccessFallbackErrorBoundary pathname="/extra-att..." notFound={<SegmentViewNode>} ...>
<RedirectBoundary>
Expand Down Expand Up @@ -221,7 +221,7 @@ describe('Error overlay for hydration errors in App router', () => {
<ScrollAndFocusHandler segmentPath={[...]}>
<InnerScrollAndFocusHandler segmentPath={[...]} focusAndScrollRef={{apply:false, ...}}>
<ErrorBoundary errorComponent={undefined} errorStyles={undefined} errorScripts={undefined}>
<LoadingBoundary loading={null}>
<LoadingBoundary name={undefined} loading={null}>
<HTTPAccessFallbackBoundary notFound={undefined} forbidden={undefined} unauthorized={undefined}>
<RedirectBoundary>
<RedirectErrorBoundary router={{...}}>
Expand Down Expand Up @@ -262,7 +262,7 @@ describe('Error overlay for hydration errors in App router', () => {
<ScrollAndFocusHandler segmentPath={[...]}>
<InnerScrollAndFocusHandler segmentPath={[...]} focusAndScrollRef={{apply:false, ...}}>
<ErrorBoundary errorComponent={undefined} errorStyles={undefined} errorScripts={undefined}>
<LoadingBoundary loading={null}>
<LoadingBoundary name={undefined} loading={null}>
<HTTPAccessFallbackBoundary notFound={undefined} forbidden={undefined} unauthorized={undefined}>
<RedirectBoundary>
<RedirectErrorBoundary router={{...}}>
Expand Down Expand Up @@ -300,7 +300,7 @@ describe('Error overlay for hydration errors in App router', () => {
<ScrollAndFocusHandler segmentPath={[...]}>
<InnerScrollAndFocusHandler segmentPath={[...]} focusAndScrollRef={{apply:false, ...}}>
<ErrorBoundary errorComponent={undefined} errorStyles={undefined} errorScripts={undefined}>
<LoadingBoundary loading={null}>
<LoadingBoundary name={undefined} loading={null}>
<HTTPAccessFallbackBoundary notFound={undefined} forbidden={undefined} unauthorized={undefined}>
<RedirectBoundary>
<RedirectErrorBoundary router={{...}}>
Expand Down Expand Up @@ -342,7 +342,7 @@ describe('Error overlay for hydration errors in App router', () => {
<ScrollAndFocusHandler segmentPath={[...]}>
<InnerScrollAndFocusHandler segmentPath={[...]} focusAndScrollRef={{apply:false, ...}}>
<ErrorBoundary errorComponent={undefined} errorStyles={undefined} errorScripts={undefined}>
<LoadingBoundary loading={null}>
<LoadingBoundary name={undefined} loading={null}>
<HTTPAccessFallbackBoundary notFound={undefined} forbidden={undefined} unauthorized={undefined}>
<RedirectBoundary>
<RedirectErrorBoundary router={{...}}>
Expand Down Expand Up @@ -376,7 +376,7 @@ describe('Error overlay for hydration errors in App router', () => {
<ScrollAndFocusHandler segmentPath={[...]}>
<InnerScrollAndFocusHandler segmentPath={[...]} focusAndScrollRef={{apply:false, ...}}>
<ErrorBoundary errorComponent={undefined} errorStyles={undefined} errorScripts={undefined}>
<LoadingBoundary loading={null}>
<LoadingBoundary name={undefined} loading={null}>
<HTTPAccessFallbackBoundary notFound={undefined} forbidden={undefined} unauthorized={undefined}>
<RedirectBoundary>
<RedirectErrorBoundary router={{...}}>
Expand Down Expand Up @@ -415,7 +415,7 @@ describe('Error overlay for hydration errors in App router', () => {
<ScrollAndFocusHandler segmentPath={[...]}>
<InnerScrollAndFocusHandler segmentPath={[...]} focusAndScrollRef={{apply:false, ...}}>
<ErrorBoundary errorComponent={undefined} errorStyles={undefined} errorScripts={undefined}>
<LoadingBoundary loading={null}>
<LoadingBoundary name={undefined} loading={null}>
<HTTPAccessFallbackBoundary notFound={undefined} forbidden={undefined} unauthorized={undefined}>
<RedirectBoundary>
<RedirectErrorBoundary router={{...}}>
Expand Down Expand Up @@ -453,7 +453,7 @@ describe('Error overlay for hydration errors in App router', () => {
"componentStack": "...
<InnerScrollAndFocusHandler segmentPath={[...]} focusAndScrollRef={{apply:false, ...}}>
<ErrorBoundary errorComponent={undefined} errorStyles={undefined} errorScripts={undefined}>
<LoadingBoundary loading={null}>
<LoadingBoundary name={undefined} loading={null}>
<HTTPAccessFallbackBoundary notFound={undefined} forbidden={undefined} unauthorized={undefined}>
<RedirectBoundary>
<RedirectErrorBoundary router={{...}}>
Expand Down Expand Up @@ -510,7 +510,7 @@ describe('Error overlay for hydration errors in App router', () => {
<ScrollAndFocusHandler segmentPath={[...]}>
<InnerScrollAndFocusHandler segmentPath={[...]} focusAndScrollRef={{apply:false, ...}}>
<ErrorBoundary errorComponent={undefined} errorStyles={undefined} errorScripts={undefined}>
<LoadingBoundary loading={null}>
<LoadingBoundary name={undefined} loading={null}>
<HTTPAccessFallbackBoundary notFound={undefined} forbidden={undefined} unauthorized={undefined}>
<RedirectBoundary>
<RedirectErrorBoundary router={{...}}>
Expand Down Expand Up @@ -566,7 +566,7 @@ describe('Error overlay for hydration errors in App router', () => {
<ScrollAndFocusHandler segmentPath={[...]}>
<InnerScrollAndFocusHandler segmentPath={[...]} focusAndScrollRef={{apply:false, ...}}>
<ErrorBoundary errorComponent={undefined} errorStyles={undefined} errorScripts={undefined}>
<LoadingBoundary loading={null}>
<LoadingBoundary name={undefined} loading={null}>
<HTTPAccessFallbackBoundary notFound={undefined} forbidden={undefined} unauthorized={undefined}>
<RedirectBoundary>
<RedirectErrorBoundary router={{...}}>
Expand Down Expand Up @@ -625,7 +625,7 @@ describe('Error overlay for hydration errors in App router', () => {
<ScrollAndFocusHandler segmentPath={[...]}>
<InnerScrollAndFocusHandler segmentPath={[...]} focusAndScrollRef={{apply:false, ...}}>
<ErrorBoundary errorComponent={undefined} errorStyles={undefined} errorScripts={undefined}>
<LoadingBoundary loading={null}>
<LoadingBoundary name={undefined} loading={null}>
<HTTPAccessFallbackBoundary notFound={undefined} forbidden={undefined} unauthorized={undefined}>
<RedirectBoundary>
<RedirectErrorBoundary router={{...}}>
Expand Down Expand Up @@ -682,7 +682,7 @@ describe('Error overlay for hydration errors in App router', () => {
<ScrollAndFocusHandler segmentPath={[...]}>
<InnerScrollAndFocusHandler segmentPath={[...]} focusAndScrollRef={{apply:false, ...}}>
<ErrorBoundary errorComponent={undefined} errorStyles={undefined} errorScripts={undefined}>
<LoadingBoundary loading={null}>
<LoadingBoundary name={undefined} loading={null}>
<HTTPAccessFallbackBoundary notFound={undefined} forbidden={undefined} unauthorized={undefined}>
<RedirectBoundary>
<RedirectErrorBoundary router={{...}}>
Expand Down Expand Up @@ -966,7 +966,7 @@ describe('Error overlay for hydration errors in App router', () => {
<ScrollAndFocusHandler segmentPath={[...]}>
<InnerScrollAndFocusHandler segmentPath={[...]} focusAndScrollRef={{apply:false, ...}}>
<ErrorBoundary errorComponent={undefined} errorStyles={undefined} errorScripts={undefined}>
<LoadingBoundary loading={null}>
<LoadingBoundary name="(script-un..." loading={null}>
<HTTPAccessFallbackBoundary notFound={<SegmentViewNode>} forbidden={undefined} unauthorized={undefined}>
<HTTPAccessFallbackErrorBoundary pathname="/script-un..." notFound={<SegmentViewNode>} ...>
<RedirectBoundary>
Expand Down
Loading
Loading