'use client'
import type { CacheNode } from '../../shared/lib/app-router-types'
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 type { FocusAndScrollRef } from './router-reducer/router-reducer-types'
import React, {
Activity,
Fragment,
useContext,
use,
Suspense,
useDeferredValue,
useLayoutEffect,
type FragmentInstance,
type JSX,
type ActivityProps,
} from 'react'
import ReactDOM from 'react-dom'
import {
LayoutRouterContext,
GlobalLayoutRouterContext,
TemplateContext,
} from '../../shared/lib/app-router-context.shared-runtime'
import { unresolvedThenable } from './unresolved-thenable'
import { ErrorBoundary } from './error-boundary'
import { disableSmoothScrollDuringRouteTransition } from '../../shared/lib/router/utils/disable-smooth-scroll'
import { RedirectBoundary } from './redirect-boundary'
import { HTTPAccessFallbackBoundary } from './http-access-fallback/error-boundary'
import { createRouterCacheKey } from './router-reducer/create-router-cache-key'
import {
useRouterBFCache,
type RouterBFCacheEntry,
} from './bfcache-state-manager'
import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths'
import {
NavigationPromisesContext,
type NavigationPromises,
} from '../../shared/lib/hooks-client-context.shared-runtime'
import { getParamValueFromCacheKey } from '../route-params'
import type { Params } from '../../server/request/params'
import { isDeferredRsc } from './router-reducer/ppr-navigations'
const enableNewScrollHandler = process.env.__NEXT_APP_NEW_SCROLL_HANDLER
const __DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE = (
ReactDOM as any
).__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE
// TODO-APP: Replace with new React API for finding dom nodes without a `ref` when available
/**
* Wraps ReactDOM.findDOMNode with additional logic to hide React Strict Mode warning
*/
function findDOMNode(
instance: React.ReactInstance | null | undefined
): Element | Text | null {
// Tree-shake for server bundle
if (typeof window === 'undefined') return null
// __DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE.findDOMNode is null during module init.
// We need to lazily reference it.
const internal_reactDOMfindDOMNode =
__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE.findDOMNode
return internal_reactDOMfindDOMNode(instance)
}
const rectProperties = [
'bottom',
'height',
'left',
'right',
'top',
'width',
'x',
'y',
] as const
/**
* Check if a HTMLElement is hidden or fixed/sticky position
*/
function shouldSkipElement(element: HTMLElement) {
// we ignore fixed or sticky positioned elements since they'll likely pass the "in-viewport" check
// and will result in a situation we bail on scroll because of something like a fixed nav,
// even though the actual page content is offscreen
if (['sticky', 'fixed'].includes(getComputedStyle(element).position)) {
return true
}
// Uses `getBoundingClientRect` to check if the element is hidden instead of `offsetParent`
// because `offsetParent` doesn't consider document/body
const rect = element.getBoundingClientRect()
return rectProperties.every((item) => rect[item] === 0)
}
/**
* Check if the top corner of the HTMLElement is in the viewport.
*/
function topOfElementInViewport(
instance: HTMLElement | FragmentInstance,
viewportHeight: number
): boolean {
const rects = instance.getClientRects()
if (rects.length === 0) {
// Just to be explicit.
return false
}
let elementTop = Number.POSITIVE_INFINITY
for (let i = 0; i < rects.length; i++) {
const rect = rects[i]
if (rect.top < elementTop) {
elementTop = rect.top
}
}
return elementTop >= 0 && elementTop <= viewportHeight
}
/**
* Find the DOM node for a hash fragment.
* If `top` the page has to scroll to the top of the page. This mirrors the browser's behavior.
* If the hash fragment is an id, the page has to scroll to the element with that id.
* If the hash fragment is a name, the page has to scroll to the first element with that name.
*/
function getHashFragmentDomNode(hashFragment: string) {
// If the hash fragment is `top` the page has to scroll to the top of the page.
if (hashFragment === 'top') {
return document.body
}
// If the hash fragment is an id, the page has to scroll to the element with that id.
return (
document.getElementById(hashFragment) ??
// If the hash fragment is a name, the page has to scroll to the first element with that name.
document.getElementsByName(hashFragment)[0]
)
}
interface ScrollAndMaybeFocusHandlerProps {
focusAndScrollRef: FocusAndScrollRef
children: React.ReactNode
cacheNode: CacheNode
}
class InnerScrollAndFocusHandlerOld extends React.Component<ScrollAndMaybeFocusHandlerProps> {
handlePotentialScroll = () => {
// Handle scroll and focus, it's only applied once.
const { focusAndScrollRef, cacheNode } = this.props
const scrollRef = focusAndScrollRef.forceScroll
? focusAndScrollRef.scrollRef
: cacheNode.scrollRef
if (scrollRef === null || !scrollRef.current) return
let domNode:
| ReturnType<typeof getHashFragmentDomNode>
| ReturnType<typeof findDOMNode> = null
const hashFragment = focusAndScrollRef.hashFragment
if (hashFragment) {
domNode = getHashFragmentDomNode(hashFragment)
}
// `findDOMNode` is tricky because it returns just the first child if the component is a fragment.
// This already caused a bug where the first child was a <link/> in head.
if (!domNode) {
domNode = findDOMNode(this)
}
// If there is no DOM node this layout-router level is skipped. It'll be handled higher-up in the tree.
if (!(domNode instanceof Element)) {
return
}
// Verify if the element is a HTMLElement and if we want to consider it for scroll behavior.
// If the element is skipped, try to select the next sibling and try again.
while (!(domNode instanceof HTMLElement) || shouldSkipElement(domNode)) {
if (process.env.NODE_ENV !== 'production') {
if (domNode.parentElement?.localName === 'head') {
// We enter this state when metadata was rendered as part of the page or via Next.js.
// This is always a bug in Next.js and caused by React hoisting metadata.
// Fixed with `experimental.appNewScrollHandler`
}
}
// No siblings found that match the criteria are found, so handle scroll higher up in the tree instead.
if (domNode.nextElementSibling === null) {
return
}
domNode = domNode.nextElementSibling
}
// Mark as scrolled so no other segment scrolls for this navigation.
scrollRef.current = false
disableSmoothScrollDuringRouteTransition(
() => {
// In case of hash scroll, we only need to scroll the element into view
if (hashFragment) {
domNode.scrollIntoView()
return
}
// Store the current viewport height because reading `clientHeight` causes a reflow,
// and it won't change during this function.
const htmlElement = document.documentElement
const viewportHeight = htmlElement.clientHeight
// If the element's top edge is already in the viewport, exit early.
if (topOfElementInViewport(domNode, viewportHeight)) {
return
}
// Otherwise, try scrolling go the top of the document to be backward compatible with pages
// scrollIntoView() called on `<html/>` element scrolls horizontally on chrome and firefox (that shouldn't happen)
// We could use it to scroll horizontally following RTL but that also seems to be broken - it will always scroll left
// scrollLeft = 0 also seems to ignore RTL and manually checking for RTL is too much hassle so we will scroll just vertically
htmlElement.scrollTop = 0
// Scroll to domNode if domNode is not in viewport when scrolled to top of document
if (!topOfElementInViewport(domNode, viewportHeight)) {
// Scroll into view doesn't scroll horizontally by default when not needed
domNode.scrollIntoView()
}
},
{
// We will force layout by querying domNode position
dontForceLayout: true,
onlyHashChange: focusAndScrollRef.onlyHashChange,
}
)
// Mutate after scrolling so that it can be read by `disableSmoothScrollDuringRouteTransition`
focusAndScrollRef.onlyHashChange = false
focusAndScrollRef.hashFragment = null
// Set focus on the element
domNode.focus()
}
componentDidMount() {
this.handlePotentialScroll()
}
componentDidUpdate() {
this.handlePotentialScroll()
}
render() {
return this.props.children
}
}
/**
* Fork of InnerScrollAndFocusHandlerOld using Fragment refs for scrolling.
* No longer focuses the first host descendant.
*/
function InnerScrollHandlerNew(props: ScrollAndMaybeFocusHandlerProps) {
const childrenRef = React.useRef<FragmentInstance>(null)
useLayoutEffect(
() => {
const { focusAndScrollRef, cacheNode } = props
const scrollRef = focusAndScrollRef.forceScroll
? focusAndScrollRef.scrollRef
: cacheNode.scrollRef
if (scrollRef === null || !scrollRef.current) return
let instance: FragmentInstance | HTMLElement | null = null
const hashFragment = focusAndScrollRef.hashFragment
if (hashFragment) {
instance = getHashFragmentDomNode(hashFragment)
}
if (!instance) {
instance = childrenRef.current
}
// If there is no DOM node this layout-router level is skipped. It'll be handled higher-up in the tree.
if (instance === null) {
return
}
// Mark as scrolled so no other segment scrolls for this navigation.
scrollRef.current = false
const activeElement = document.activeElement
if (
activeElement !== null &&
'blur' in activeElement &&
typeof activeElement.blur === 'function'
) {
// Trying to match hard navigations.
// Ideally we'd move the internal focus cursor either to the top
// or at least before the segment. But there's no DOM API to do that,
// so we just blur.
// We could workaround this by moving focus to a temporary element in
// the body. But adding elements might trigger layout or other effects
// so it should be well motivated.
activeElement.blur()
}
disableSmoothScrollDuringRouteTransition(
() => {
// In case of hash scroll, we only need to scroll the element into view
if (hashFragment) {
instance.scrollIntoView()
return
}
// Store the current viewport height because reading `clientHeight` causes a reflow,
// and it won't change during this function.
const htmlElement = document.documentElement
const viewportHeight = htmlElement.clientHeight
// If the element's top edge is already in the viewport, exit early.
if (topOfElementInViewport(instance, viewportHeight)) {
return
}
// Otherwise, try scrolling go the top of the document to be backward compatible with pages
// scrollIntoView() called on `<html/>` element scrolls horizontally on chrome and firefox (that shouldn't happen)
// We could use it to scroll horizontally following RTL but that also seems to be broken - it will always scroll left
// scrollLeft = 0 also seems to ignore RTL and manually checking for RTL is too much hassle so we will scroll just vertically
htmlElement.scrollTop = 0
// Scroll to domNode if domNode is not in viewport when scrolled to top of document
if (!topOfElementInViewport(instance, viewportHeight)) {
// Scroll into view doesn't scroll horizontally by default when not needed
instance.scrollIntoView()
}
},
{
// We will force layout by querying domNode position
dontForceLayout: true,
onlyHashChange: focusAndScrollRef.onlyHashChange,
}
)
// Mutate after scrolling so that it can be read by `disableSmoothScrollDuringRouteTransition`
focusAndScrollRef.onlyHashChange = false
focusAndScrollRef.hashFragment = null
},
// Used to run on every commit. We may be able to be smarter about this
// but be prepared for lots of manual testing.
undefined
)
return <Fragment ref={childrenRef}>{props.children}</Fragment>
}
const InnerScrollAndMaybeFocusHandler = enableNewScrollHandler
? InnerScrollHandlerNew
: InnerScrollAndFocusHandlerOld
function ScrollAndMaybeFocusHandler({
children,
cacheNode,
}: {
children: React.ReactNode
cacheNode: CacheNode
}) {
const context = useContext(GlobalLayoutRouterContext)
if (!context) {
throw new Error('invariant global layout router not mounted')
}
return (
<InnerScrollAndMaybeFocusHandler
focusAndScrollRef={context.focusAndScrollRef}
cacheNode={cacheNode}
>
{children}
</InnerScrollAndMaybeFocusHandler>
)
}
/**
* InnerLayoutRouter handles rendering the provided segment based on the cache.
*/
function InnerLayoutRouter({
tree,
segmentPath,
debugNameContext,
cacheNode: maybeCacheNode,
params,
url,
isActive,
}: {
tree: FlightRouterState
segmentPath: FlightSegmentPath
debugNameContext: string
cacheNode: CacheNode | null
params: Params
url: string
isActive: boolean
}) {
const context = useContext(GlobalLayoutRouterContext)
const parentNavPromises = useContext(NavigationPromisesContext)
if (!context) {
throw new Error('invariant global layout router not mounted')
}
const cacheNode =
maybeCacheNode !== null
? maybeCacheNode
: // This segment is not in the cache. Suspend indefinitely.
//
// This should only be reachable for inactive/hidden segments, during
// prerendering The active segment should always be consistent with the
// CacheNode tree. Regardless, if we don't have a matching CacheNode, we
// must suspend rather than render nothing, to prevent showing an
// inconsistent route.
(use(unresolvedThenable) as never)
// `rsc` represents the renderable node for this segment.
// If this segment has a `prefetchRsc`, it's the statically prefetched data.
// We should use that on initial render instead of `rsc`. Then we'll switch
// to `rsc` when the dynamic response streams in.
//
// If no prefetch data is available, then we go straight to rendering `rsc`.
const resolvedPrefetchRsc =
cacheNode.prefetchRsc !== null ? cacheNode.prefetchRsc : cacheNode.rsc
// We use `useDeferredValue` to handle switching between the prefetched and
// final values. The second argument is returned on initial render, then it
// re-renders with the first argument.
const rsc: any = useDeferredValue(cacheNode.rsc, resolvedPrefetchRsc)
// `rsc` is either a React node or a promise for a React node, except we
// special case `null` to represent that this segment's data is missing. If
// it's a promise, we need to unwrap it so we can determine whether or not the
// data is missing.
let resolvedRsc: React.ReactNode
if (isDeferredRsc(rsc)) {
const unwrappedRsc = use(rsc)
if (unwrappedRsc === null) {
// If the promise was resolved to `null`, it means the data for this
// segment was not returned by the server. Suspend indefinitely. When this
// happens, the router is responsible for triggering a new state update to
// un-suspend this segment.
use(unresolvedThenable) as never
}
resolvedRsc = unwrappedRsc
} else {
// This is not a deferred RSC promise. Don't need to unwrap it.
if (rsc === null) {
use(unresolvedThenable) as never
}
resolvedRsc = rsc
}
// In dev, we create a NavigationPromisesContext containing the instrumented promises that provide
// `useSelectedLayoutSegment` and `useSelectedLayoutSegments`.
// Promises are cached outside of render to survive suspense retries.
let navigationPromises: NavigationPromises | null = null
if (process.env.NODE_ENV !== 'production') {
const { createNestedLayoutNavigationPromises } =
require('./navigation-devtools') as typeof import('./navigation-devtools')
navigationPromises = createNestedLayoutNavigationPromises(
tree,
parentNavPromises
)
}
let children = resolvedRsc
if (navigationPromises) {
children = (
<NavigationPromisesContext.Provider value={navigationPromises}>
{resolvedRsc}
</NavigationPromisesContext.Provider>
)
}
children = (
// The layout router context narrows down tree and childNodes at each level.
<LayoutRouterContext.Provider
value={{
parentTree: tree,
parentCacheNode: cacheNode,
parentSegmentPath: segmentPath,
parentParams: params,
// This is always set to null as we enter a child segment. It's
// populated by LoadingBoundaryProvider the next time we reach a
// loading boundary.
parentLoadingData: null,
debugNameContext: debugNameContext,
// TODO-APP: overriding of url for parallel routes
url: url,
isActive: isActive,
}}
>
{children}
</LayoutRouterContext.Provider>
)
return children
}
export function LoadingBoundaryProvider({
loading,
children,
}: {
loading: LoadingModuleData
children: React.ReactNode
}) {
// Provides the data needed to render a loading.tsx boundary, via context.
//
// loading.tsx creates a Suspense boundary around each of a layout's child
// slots. (Might be bit confusing to think about the data flow, but: if
// loading.tsx and layout.tsx are in the same directory, they are assigned
// to the same CacheNode.)
//
// This provider component does not render the Suspense boundary directly;
// that's handled by LoadingBoundary.
//
// TODO: For simplicity, we should combine this provider with LoadingBoundary
// and render the Suspense boundary directly. The only real benefit of doing
// it separately is so that when there are multiple parallel routes, we only
// send the boundary data once, rather than once per child. But that's a
// negligible benefit and can be achieved via caching instead.
const parentContext = use(LayoutRouterContext)
if (parentContext === null) {
return children
}
// All values except for parentLoadingData are the same as the parent context.
return (
<LayoutRouterContext.Provider
value={{
parentTree: parentContext.parentTree,
parentCacheNode: parentContext.parentCacheNode,
parentSegmentPath: parentContext.parentSegmentPath,
parentParams: parentContext.parentParams,
parentLoadingData: loading,
debugNameContext: parentContext.debugNameContext,
url: parentContext.url,
isActive: parentContext.isActive,
}}
>
{children}
</LayoutRouterContext.Provider>
)
}
/**
* Renders suspense boundary with the provided "loading" property as the fallback.
* If no loading property is provided it renders the children without a suspense boundary.
*/
function LoadingBoundary({
name,
loading,
children,
}: {
name: ActivityProps['name']
loading: LoadingModuleData | null
children: React.ReactNode
}): JSX.Element {
// TODO: For LoadingBoundary, and the other built-in boundary types, don't
// wrap in an extra function component if no user-defined boundary is
// provided. In other words, inline this conditional wrapping logic into
// the parent component. More efficient and keeps unnecessary junk out of
// the component stack.
if (loading !== null) {
const loadingRsc = loading[0]
const loadingStyles = loading[1]
const loadingScripts = loading[2]
return (
<Suspense
name={name}
fallback={
<>
{loadingStyles}
{loadingScripts}
{loadingRsc}
</>
}
>
{children}
</Suspense>
)
}
return <>{children}</>
}
/**
* OuterLayoutRouter handles the current segment as well as <Offscreen> rendering of other segments.
* It can be rendered next to each other with a different `parallelRouterKey`, allowing for Parallel routes.
*/
export default function OuterLayoutRouter({
parallelRouterKey,
error,
errorStyles,
errorScripts,
templateStyles,
templateScripts,
template,
notFound,
forbidden,
unauthorized,
segmentViewBoundaries,
}: {
parallelRouterKey: string
error: ErrorComponent | undefined
errorStyles: React.ReactNode | undefined
errorScripts: React.ReactNode | undefined
templateStyles: React.ReactNode | undefined
templateScripts: React.ReactNode | undefined
template: React.ReactNode
notFound: React.ReactNode | undefined
forbidden: React.ReactNode | undefined
unauthorized: React.ReactNode | undefined
segmentViewBoundaries?: React.ReactNode
}) {
const context = useContext(LayoutRouterContext)
if (!context) {
throw new Error('invariant expected layout router to be mounted')
}
const {
parentTree,
parentCacheNode,
parentSegmentPath,
parentParams,
parentLoadingData,
url,
isActive,
debugNameContext,
} = context
// Get the CacheNode for this segment by reading it from the parent segment's
// child map.
const parentTreeSegment = parentTree[0]
const segmentPath =
parentSegmentPath === null
? // TODO: The root segment value is currently omitted from the segment
// path. This has led to a bunch of special cases scattered throughout
// the code. We should clean this up.
[parallelRouterKey]
: parentSegmentPath.concat([parentTreeSegment, parallelRouterKey])
// The "state" key of a segment is the one passed to React — it represents the
// identity of the UI tree. Whenever the state key changes, the tree is
// recreated and the state is reset. In the App Router model, search params do
// not cause state to be lost, so two segments with the same segment path but
// different search params should have the same state key.
//
// The "cache" key of a segment, however, *does* include the search params, if
// it's possible that the segment accessed the search params on the server.
// (This only applies to page segments; layout segments cannot access search
// params on the server.)
const activeTree = parentTree[1][parallelRouterKey]
const maybeParentSlots = parentCacheNode.slots
if (activeTree === undefined || maybeParentSlots === null) {
// Could not find a matching segment. The client tree is inconsistent with
// the server tree. Suspend indefinitely; the router will have already
// detected the inconsistency when handling the server response, and
// triggered a refresh of the page to recover.
use(unresolvedThenable) as never
}
let maybeValidationBoundaryId: string | null = null
if (typeof window === 'undefined' && process.env.__NEXT_CACHE_COMPONENTS) {
const { InstantValidationBoundaryContext } =
require('./instant-validation/boundary') as typeof import('./instant-validation/boundary')
maybeValidationBoundaryId = use(InstantValidationBoundaryContext)
}
const activeSegment = activeTree[0]
const activeCacheNode = maybeParentSlots![parallelRouterKey] ?? null
const activeStateKey = createRouterCacheKey(activeSegment, true) // no search params
// At each level of the route tree, not only do we render the currently
// active segment — we also render the last N segments that were active at
// this level inside a hidden <Activity> boundary, to preserve their state
// if or when the user navigates to them again.
//
// bfcacheEntry is a linked list of FlightRouterStates.
let bfcacheEntry: RouterBFCacheEntry | null = useRouterBFCache(
activeTree,
activeCacheNode,
activeStateKey
)
let children: Array<React.ReactNode> = []
do {
const tree = bfcacheEntry.tree
const cacheNode = bfcacheEntry.cacheNode
const stateKey = bfcacheEntry.stateKey
const segment = tree[0]
/*
- Error boundary
- Only renders error boundary if error component is provided.
- Rendered for each segment to ensure they have their own error state.
- When gracefully degrade for bots, skip rendering error boundary.
- Loading boundary
- Only renders suspense boundary if loading components is provided.
- Rendered for each segment to ensure they have their own loading state.
- Passed to the router during rendering to ensure it can be immediately rendered when suspending on a Flight fetch.
*/
let segmentBoundaryTriggerNode: React.ReactNode = null
let segmentViewStateNode: React.ReactNode = null
if (process.env.NODE_ENV !== 'production') {
const { SegmentBoundaryTriggerNode, SegmentViewStateNode } =
require('../../next-devtools/userspace/app/segment-explorer-node') as typeof import('../../next-devtools/userspace/app/segment-explorer-node')
const pagePrefix = normalizeAppPath(url)
segmentViewStateNode = (
<SegmentViewStateNode key={pagePrefix} page={pagePrefix} />
)
segmentBoundaryTriggerNode = (
<>
<SegmentBoundaryTriggerNode />
</>
)
}
let params = parentParams
if (Array.isArray(segment)) {
// This segment contains a route param. Accumulate these as we traverse
// down the router tree. The result represents the set of params that
// the layout/page components are permitted to access below this point.
const paramName = segment[0]
const paramCacheKey = segment[1]
const paramType = segment[2]
const paramValue = getParamValueFromCacheKey(paramCacheKey, paramType)
if (paramValue !== null) {
params = {
...parentParams,
[paramName]: paramValue,
}
}
}
const debugName = getBoundaryDebugNameFromSegment(segment)
// `debugNameContext` represents the nearest non-"virtual" parent segment.
// `getBoundaryDebugNameFromSegment` returns undefined for virtual segments.
// So if `debugName` is undefined, the context is passed through unchanged.
const childDebugNameContext = debugName ?? debugNameContext
// 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.
const isVirtual = debugName === undefined
const debugNameToDisplay = isVirtual ? undefined : debugNameContext
let templateValue = (
<ScrollAndMaybeFocusHandler cacheNode={cacheNode}>
<ErrorBoundary
errorComponent={error}
errorStyles={errorStyles}
errorScripts={errorScripts}
>
<LoadingBoundary
name={debugNameToDisplay}
// 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), this is no
// different from if the loading module data were stored on the
// child directly. But I'm not sure this actually makes sense
// when there are multiple parallel routes. It's not a huge
// issue because you always have the option to define a narrower
// loading boundary for a particular slot. But this sort of
// smells like an implementation accident to me.
loading={parentLoadingData}
>
<HTTPAccessFallbackBoundary
notFound={notFound}
forbidden={forbidden}
unauthorized={unauthorized}
>
<RedirectBoundary>
<InnerLayoutRouter
url={url}
tree={tree}
params={params}
cacheNode={cacheNode}
segmentPath={segmentPath}
debugNameContext={childDebugNameContext}
isActive={isActive && stateKey === activeStateKey}
/>
{segmentBoundaryTriggerNode}
</RedirectBoundary>
</HTTPAccessFallbackBoundary>
</LoadingBoundary>
</ErrorBoundary>
{segmentViewStateNode}
</ScrollAndMaybeFocusHandler>
)
if (
typeof window === 'undefined' &&
process.env.__NEXT_CACHE_COMPONENTS &&
typeof maybeValidationBoundaryId === 'string'
) {
const { RenderValidationBoundaryAtThisLevel } =
require('./instant-validation/boundary') as typeof import('./instant-validation/boundary')
templateValue = (
<RenderValidationBoundaryAtThisLevel id={maybeValidationBoundaryId}>
{templateValue}
</RenderValidationBoundaryAtThisLevel>
)
}
let child = (
<TemplateContext.Provider key={stateKey} value={templateValue}>
{templateStyles}
{templateScripts}
{template}
</TemplateContext.Provider>
)
if (process.env.NODE_ENV !== 'production') {
const { SegmentStateProvider } =
require('../../next-devtools/userspace/app/segment-explorer-node') as typeof import('../../next-devtools/userspace/app/segment-explorer-node')
child = (
<SegmentStateProvider key={stateKey}>
{child}
{segmentViewBoundaries}
</SegmentStateProvider>
)
}
if (process.env.__NEXT_CACHE_COMPONENTS) {
child = (
<Activity
name={debugNameToDisplay}
key={stateKey}
mode={stateKey === activeStateKey ? 'visible' : 'hidden'}
>
{child}
</Activity>
)
}
children.push(child)
bfcacheEntry = bfcacheEntry.next
} while (bfcacheEntry !== null)
return children
}
function getBoundaryDebugNameFromSegment(segment: Segment): string | undefined {
if (segment === '/') {
// Reached the root
return '/'
}
if (typeof segment === 'string') {
if (isVirtualLayout(segment)) {
return undefined
} else {
return segment + '/'
}
}
const paramCacheKey = segment[1]
return paramCacheKey + '/'
}
function isVirtualLayout(segment: string): boolean {
return (
// This is inserted by the loader. Uses double-underscore convention
// (like __PAGE__ and __DEFAULT__) to avoid collisions with
// user-defined route groups.
segment === '(__SLOT__)'
)
}