'use client' import * as React from 'react' import { useSelector } from '@tanstack/react-store' import { isNotFound, rootRouteId } from '@tanstack/router-core' import { isServer } from '@tanstack/router-core/isServer' import { CatchBoundary, ErrorComponent } from './CatchBoundary' import { useRouter } from './useRouter' import { CatchNotFound } from './not-found' import { matchContext } from './matchContext' import { SafeFragment } from './SafeFragment' import { renderRouteNotFound } from './renderRouteNotFound' import { ScrollRestoration } from './scroll-restoration' import { ClientOnly } from './ClientOnly' import { nonRouteComponentContext, wrapInNonRouteComponentContext, } from './nonRouteComponentContext' import type { AnyRoute, AnyRouteMatch, RootRouteOptions, } from '@tanstack/router-core' export function renderPending( router: ReturnType, route?: AnyRoute, ) { const PendingComponent = route?.options.pendingComponent ?? router.options.defaultPendingComponent if (!PendingComponent) { return null } const pendingElement = return process.env.NODE_ENV !== 'production' ? wrapInNonRouteComponentContext(pendingElement, 'pendingComponent') : pendingElement } type OutletMatchSelection = [ parentGlobalNotFound: boolean, parentNotFoundError: unknown, ] const outletMatchSelectionEqual = ( a: OutletMatchSelection, b: OutletMatchSelection, ) => a[0] === b[0] && a[1] === b[1] const canWrapInSuspense = ( router: ReturnType, route: AnyRoute, ssr: AnyRouteMatch['ssr'], ) => !route.isRoot || (route.options as RootRouteOptions).shellComponent || route.options.wrapInSuspense || ssr === false || ssr === 'data-only' || !((isServer ?? router.isServer) || router.ssr) export const Match = React.memo(function MatchImpl({ routeId, }: { routeId: string }) { const router = useRouter() if (isServer ?? router.isServer) { const match = router.stores.byRoute.get(routeId)!.get()! return } const matchStore = router.stores.getMatchStore(routeId) // eslint-disable-next-line react-hooks/rules-of-hooks const match = useSelector(matchStore) return }) function MatchView({ router, match, }: { router: ReturnType match: AnyRouteMatch }) { const route: AnyRoute = router.routesById[match.routeId] const pendingElement = renderPending(router, route) const routeErrorComponent = route.options.errorComponent ?? router.options.defaultErrorComponent const routeOnCatch = route.options.onCatch ?? router.options.defaultOnCatch const routeNotFoundComponent = route.isRoot ? // If it's the root route, use the _notFound option, with fallback to the notFoundRoute's component (route.options.notFoundComponent ?? router.options.notFoundRoute?.options.component) : route.options.notFoundComponent const resolvedNoSsr = match.ssr === false || match.ssr === 'data-only' // A root component may render the document itself. Only place its Suspense // boundary in pure CSR, inside an explicit shell, or when explicitly opted in. const ResolvedSuspenseBoundary = canWrapInSuspense(router, route, match.ssr) && (route.options.wrapInSuspense ?? pendingElement ?? ((route.options.errorComponent as any)?.preload || resolvedNoSsr)) ? React.Suspense : SafeFragment const ResolvedCatchBoundary = routeErrorComponent ? CatchBoundary : SafeFragment const ResolvedNotFoundBoundary = routeNotFoundComponent ? CatchNotFound : SafeFragment const ShellComponent = route.isRoot ? ((route.options as RootRouteOptions).shellComponent ?? SafeFragment) : SafeFragment return ( match} errorComponent={routeErrorComponent as any} onCatch={(error, errorInfo) => { // Forward not found errors (we don't want to show the error component for these) if (isNotFound(error)) { error.routeId ??= match.routeId throw error } if (process.env.NODE_ENV !== 'production') { console.warn(`Warning: Error in route match: ${match.id}`) } routeOnCatch?.(error, errorInfo) }} > { error.routeId ??= match.routeId if (error.routeId !== match.routeId) { throw error } const notFoundElement = React.createElement( routeNotFoundComponent!, error as any, ) return process.env.NODE_ENV !== 'production' ? wrapInNonRouteComponentContext( notFoundElement, 'notFoundComponent', ) : notFoundElement }} > {resolvedNoSsr ? ( ) : ( )} {(isServer ?? router.isServer) && route.parentRoute?.id === rootRouteId && router.options.scrollRestoration ? ( ) : null} ) } export const MatchInner = React.memo(function MatchInnerImpl({ match, }: { match: AnyRouteMatch }): any { const router = useRouter() const routeId = match.routeId const route = router.routesById[routeId] as AnyRoute const key = React.useMemo(() => { const remountFn = route.options.remountDeps ?? router.options.defaultRemountDeps const remountDeps = remountFn?.({ routeId, loaderDeps: match.loaderDeps, params: match._strictParams, search: match._strictSearch, }) return remountDeps ? JSON.stringify(remountDeps) : undefined }, [ routeId, match.loaderDeps, match._strictParams, match._strictSearch, route.options.remountDeps, router.options.defaultRemountDeps, ]) const out = React.useMemo(() => { const Comp = route.options.component ?? router.options.defaultComponent return Comp ? : }, [key, route.options.component, router.options.defaultComponent]) if (match.status === 'pending') { if (router.ssr && !canWrapInSuspense(router, route, match.ssr)) { // Replacing an SSR document root with pending UI would remove . // Hydrated matches retain their prior data, so keep rendering it. return out } if (router._tx) { throw router._tx[5] } return renderPending(router, route) } if (match.status === 'notFound') { return renderRouteNotFound(router, route, match.error) } if (match.status === 'error') { if (isServer ?? router.isServer) { const RouteErrorComponent = (route.options.errorComponent ?? router.options.defaultErrorComponent) || ErrorComponent const errorElement = ( ) return process.env.NODE_ENV !== 'production' ? wrapInNonRouteComponentContext(errorElement, 'errorComponent') : errorElement } throw match.error } return out }) /** * Render the next child match in the route tree. Typically used inside * a route component to render nested routes. * * @link https://tanstack.com/router/latest/docs/framework/react/api/router/outletComponent */ export const Outlet = React.memo(function OutletImpl() { if (process.env.NODE_ENV !== 'production') { // eslint-disable-next-line react-hooks/rules-of-hooks const nonRouteComponent = React.useContext(nonRouteComponentContext!) if (nonRouteComponent) { console.warn( `Warning: An was rendered inside a ${nonRouteComponent}. should only be rendered inside a route component.`, ) } } const router = useRouter() const routeId = React.useContext(matchContext)! let parentGlobalNotFound: boolean let parentNotFoundError: unknown let childRouteId: string | undefined if (isServer ?? router.isServer) { const matches = router.stores.matches.get() const parentIndex = matches.findIndex((match) => match.routeId === routeId) const parentMatch = matches[parentIndex]! parentGlobalNotFound = !!parentMatch._notFound parentNotFoundError = parentMatch.error childRouteId = matches[parentIndex + 1]?.routeId } else { const parentMatchStore = router.stores.getMatchStore(routeId) // eslint-disable-next-line react-hooks/rules-of-hooks ;[parentGlobalNotFound, parentNotFoundError] = useSelector( parentMatchStore, (match): OutletMatchSelection => [!!match!._notFound, match!.error], { compare: outletMatchSelectionEqual }, ) // eslint-disable-next-line react-hooks/rules-of-hooks childRouteId = useSelector(router.stores.ids, (ids) => { return ids[ids.indexOf(routeId) + 1] }) } if (parentGlobalNotFound) { return renderRouteNotFound( router, router.routesById[routeId], parentNotFoundError, ) } if (!childRouteId) { return null } const nextMatch = if (routeId === rootRouteId) { return ( {nextMatch} ) } return nextMatch })