UNPKG

@tanstack/solid-router

Version:

Modern and scalable routing for Solid applications

281 lines (280 loc) 14.2 kB
import * as Solid from 'solid-js'; import { rootRouteId } from '@tanstack/router-core'; import { isServer } from '@tanstack/router-core/isServer'; import { Dynamic } from '@solidjs/web'; import { CatchBoundary, ErrorComponent } from './CatchBoundary'; import { useRouter } from './useRouter'; import { CatchNotFound, getNotFound } from './not-found'; import { nearestMatchContext } from './matchContext'; import { SafeFragment } from './SafeFragment'; import { renderRouteNotFound } from './renderRouteNotFound'; import { ScrollRestoration } from './scroll-restoration'; import { ClientOnly } from './ClientOnly'; import { nonRouteComponentContext, renderInNonRouteComponentContext, } from './nonRouteComponentContext'; const NearestMatchContext = nearestMatchContext; // Keep the client constant undefined so the server-only script can be dropped. const renderScrollRestoration = isServer === false ? undefined : (router, route) => (isServer ?? router.isServer) && route.parentRoute?.id === rootRouteId && router.options.scrollRestoration ? (<ScrollRestoration />) : null; export const Match = (props) => { const router = useRouter(); const currentMatch = Solid.createMemo(() => router.stores.getMatchStore(props.routeId).get()); const matchState = Solid.createMemo(() => { const match = currentMatch(); if (!match) { return null; } return { routeId: match.routeId, ssr: match.ssr, status: match.status, }; }); const nearestMatch = [() => props.routeId, currentMatch]; return (<Solid.Show when={matchState()}> {(currentMatchState) => { const route = router.routesById[props.routeId]; const routeOptions = () => { // Lazy route options become observable with the next publication. currentMatchState(); return route.options; }; const resolvePendingComponent = Solid.createMemo(() => routeOptions().pendingComponent ?? router.options.defaultPendingComponent); const routeErrorComponent = Solid.createMemo(() => routeOptions().errorComponent ?? router.options.defaultErrorComponent); const routeNotFoundComponent = Solid.createMemo(() => route.isRoot ? (routeOptions().notFoundComponent ?? router.options.notFoundRoute?.options.component ?? router.options.defaultNotFoundComponent) : (routeOptions().notFoundComponent ?? router.options.defaultNotFoundComponent)); const resolvedNoSsr = Solid.createMemo(() => currentMatchState().ssr === false || currentMatchState().ssr === 'data-only'); const ResolvedLoadingBoundary = Solid.createMemo(() => resolvedNoSsr() ? SafeFragment : Solid.Loading); const shouldSkipLoadingFallback = Solid.createMemo(() => (isServer ?? router.isServer) ? resolvedNoSsr() : currentMatchState().ssr === 'data-only'); const ResolvedNotFoundBoundary = Solid.createMemo(() => routeNotFoundComponent() ? CatchNotFound : SafeFragment); const ShellComponent = Solid.createMemo(() => route.isRoot ? (route.options.shellComponent ?? SafeFragment) : SafeFragment); const MatchContent = () => (<Solid.Show when={currentMatchState().status !== 'pending'} fallback={(() => { if (process.env.NODE_ENV !== 'production') { return renderInNonRouteComponentContext(() => <Dynamic component={resolvePendingComponent()}/>, 'pendingComponent'); } return <Dynamic component={resolvePendingComponent()}/>; })()}> <MatchInner /> </Solid.Show>); const RouteContent = () => (<Dynamic component={ResolvedNotFoundBoundary()} fallback={(error) => { const notFoundError = getNotFound(error) ?? error; notFoundError.routeId ?? (notFoundError.routeId = currentMatchState().routeId); if (notFoundError.routeId !== currentMatchState().routeId) { throw notFoundError; } return process.env.NODE_ENV !== 'production' ? (renderInNonRouteComponentContext(() => (<Dynamic component={routeNotFoundComponent()} {...notFoundError}/>), 'notFoundComponent')) : (<Dynamic component={routeNotFoundComponent()} {...notFoundError}/>); }}> <Solid.Switch> <Solid.Match when={resolvedNoSsr()}> <ClientOnly fallback={(() => { if (process.env.NODE_ENV !== 'production') { return renderInNonRouteComponentContext(() => <Dynamic component={resolvePendingComponent()}/>, 'pendingComponent'); } return <Dynamic component={resolvePendingComponent()}/>; })()}> <MatchContent /> </ClientOnly> </Solid.Match> <Solid.Match when={!resolvedNoSsr()}> <MatchContent /> </Solid.Match> </Solid.Switch> </Dynamic>); return (<Dynamic component={ShellComponent()}> <NearestMatchContext value={nearestMatch}> <Dynamic component={ResolvedLoadingBoundary()} fallback={(() => { // Data-only SSR renders the inner fallback on the server, so // avoid adding an extra loading fallback on the client. if (shouldSkipLoadingFallback()) { return undefined; } if (process.env.NODE_ENV !== 'production') { return renderInNonRouteComponentContext(() => <Dynamic component={resolvePendingComponent()}/>, 'pendingComponent'); } return <Dynamic component={resolvePendingComponent()}/>; })()}> <Solid.Show when={routeErrorComponent()} fallback={<RouteContent />}> {(errorComponent) => (<CatchBoundary // Scope the reset key to this match and its // descendants (whose errors bubble here when they // have no errorComponent of their own): resetting on // unrelated matches' transitions just re-throws the // still-stale error and recreates the retried // subtree mid-settle. getResetKey={() => { const matches = router.stores.matches.get(); const index = matches.findIndex((match) => match.routeId === props.routeId); if (index === -1) { return ''; } let key = ''; for (let i = index; i < matches.length; i++) { const match = matches[i]; key += `${match.status}|${match.updatedAt},`; } return key; }} errorComponent={errorComponent()} onCatch={(error) => { const notFoundError = getNotFound(error); if (notFoundError) { notFoundError.routeId ?? (notFoundError.routeId = currentMatchState().routeId); throw notFoundError; } if (process.env.NODE_ENV !== 'production') { console.warn(`Warning: Error in route match: ${currentMatchState().routeId}`); } ; (routeOptions().onCatch ?? router.options.defaultOnCatch)?.(error); }} render={RouteContent}/>)} </Solid.Show> </Dynamic> </NearestMatchContext> {renderScrollRestoration?.(router, route)} </Dynamic>); }} </Solid.Show>); }; export const MatchInner = () => { const router = useRouter(); const nearestMatch = Solid.useContext(nearestMatchContext); const match = nearestMatch[1 /* match */]; const routeId = nearestMatch[0 /* route id */]; const matchState = Solid.createMemo(() => { const currentMatch = match(); const currentRouteId = routeId(); if (!currentMatch || !currentRouteId) { return null; } const route = router.routesById[currentRouteId]; const remount = route.options.remountDeps ?? router.options.defaultRemountDeps; let componentKey; if (!remount) { componentKey = currentRouteId; } else { const deps = remount({ routeId: currentRouteId, loaderDeps: currentMatch.loaderDeps, params: currentMatch._strictParams, search: currentMatch._strictSearch, }); componentKey = JSON.stringify(deps) ?? currentRouteId; } return { route, routeId: currentRouteId, match: { id: currentMatch.id, status: currentMatch.status, error: currentMatch.error, }, componentKey, }; }); return (<Solid.Show when={matchState()}> {(currentMatchState) => { const route = Solid.createMemo(() => currentMatchState().route); const currentMatch = Solid.createMemo(() => currentMatchState().match); const componentKey = Solid.createMemo(() => currentMatchState().componentKey); const OutComponent = Solid.createMemo(() => route().options.component ?? router.options.defaultComponent ?? Outlet); const keyedOut = () => (<Solid.Show when={componentKey()} keyed> {(_key) => <Dynamic component={OutComponent()}/>} </Solid.Show>); return (<Solid.Switch> <Solid.Match when={currentMatch().status === 'notFound'}> {(_) => Solid.untrack(() => renderRouteNotFound(router, route(), currentMatch().error))} </Solid.Match> <Solid.Match when={currentMatch().status === 'error'}> {(_) => { const matchError = Solid.untrack(() => currentMatch().error); if (isServer ?? router.isServer) { const RouteErrorComponent = (route().options.errorComponent ?? router.options.defaultErrorComponent) || ErrorComponent; return process.env.NODE_ENV !== 'production' ? (renderInNonRouteComponentContext(() => (<RouteErrorComponent error={matchError} reset={undefined} info={{ componentStack: '' }}/>), 'errorComponent')) : (<RouteErrorComponent error={matchError} reset={undefined} info={{ componentStack: '' }}/>); } throw matchError; }} </Solid.Match> <Solid.Match when={currentMatch().status === 'success'}> {keyedOut()} </Solid.Match> </Solid.Switch>); }} </Solid.Show>); }; export const Outlet = () => { if (process.env.NODE_ENV !== 'production') { const nonRouteComponent = Solid.useContext(nonRouteComponentContext); if (nonRouteComponent) { console.warn(`Warning: An <Outlet /> was rendered inside a ${nonRouteComponent}. <Outlet /> should only be rendered inside a route component.`); } } const router = useRouter(); const nearestParentMatch = Solid.useContext(nearestMatchContext); const parentMatch = nearestParentMatch[1 /* match */]; const routeId = nearestParentMatch[0 /* route id */]; const route = Solid.createMemo(() => { const currentRouteId = routeId(); return currentRouteId ? router.routesById[currentRouteId] : undefined; }); const parentNotFound = Solid.createMemo(() => parentMatch()?._notFound); const parentNotFoundError = Solid.createMemo(() => parentMatch()?.error); const childRouteId = Solid.createMemo(() => { if (parentNotFound()) { return undefined; } const currentRouteId = routeId(); if (!currentRouteId) { return undefined; } const ids = router.stores.ids.get(); return ids[ids.indexOf(currentRouteId) + 1]; }); const childPendingComponent = Solid.createMemo(() => { const childId = childRouteId(); return childId ? (router.routesById[childId].options.pendingComponent ?? router.options.defaultPendingComponent) : router.options.defaultPendingComponent; }); return (<Solid.Show when={childRouteId()} keyed fallback={<Solid.Show when={parentNotFound() && route()}> {(resolvedRoute) => Solid.untrack(() => renderRouteNotFound(router, resolvedRoute(), parentNotFoundError()))} </Solid.Show>}> {(currentChildRouteId) => { const nextMatch = () => <Match routeId={currentChildRouteId}/>; return (<Solid.Show when={routeId() === rootRouteId} fallback={nextMatch()}> <Solid.Loading fallback={(() => { if (!childPendingComponent()) { return null; } if (process.env.NODE_ENV !== 'production') { return renderInNonRouteComponentContext(() => <Dynamic component={childPendingComponent()}/>, 'pendingComponent'); } return <Dynamic component={childPendingComponent()}/>; })()}> {nextMatch()} </Solid.Loading> </Solid.Show>); }} </Solid.Show>); }; //# sourceMappingURL=Match.jsx.map