UNPKG

@tanstack/solid-router

Version:

Modern and scalable routing for Solid applications

318 lines (317 loc) 15.8 kB
import * as Solid from 'solid-js'; import { createControlledPromise, getLocationChangeInfo, invariant, isNotFound, isRedirect, rootRouteId, } from '@tanstack/router-core'; import { isServer } from '@tanstack/router-core/isServer'; import { Dynamic } from 'solid-js/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'; export const Match = (props) => { const router = useRouter(); const match = Solid.createMemo(() => { const id = props.matchId; if (!id) return undefined; return router.stores.activeMatchStoresById.get(id)?.state; }); const rawMatchState = Solid.createMemo(() => { const currentMatch = match(); if (!currentMatch) { return null; } const routeId = currentMatch.routeId; const parentRouteId = router.routesById[routeId]?.parentRoute ?.id; return { matchId: currentMatch.id, routeId, ssr: currentMatch.ssr, _displayPending: currentMatch._displayPending, parentRouteId: parentRouteId, }; }); const hasPendingMatch = Solid.createMemo(() => { const currentRouteId = rawMatchState()?.routeId; return currentRouteId ? Boolean(router.stores.pendingRouteIds.state[currentRouteId]) : false; }); const nearestMatch = { matchId: () => rawMatchState()?.matchId, routeId: () => rawMatchState()?.routeId, match, hasPending: hasPendingMatch, }; return (<Solid.Show when={rawMatchState()}> {(currentMatchState) => { const route = () => router.routesById[currentMatchState().routeId]; const resolvePendingComponent = () => route().options.pendingComponent ?? router.options.defaultPendingComponent; 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 globalNotFound option, with fallback to the notFoundRoute's component (route().options.notFoundComponent ?? router.options.notFoundRoute?.options.component) : route().options.notFoundComponent; const resolvedNoSsr = currentMatchState().ssr === false || currentMatchState().ssr === 'data-only'; const ResolvedSuspenseBoundary = () => Solid.Suspense; const ResolvedCatchBoundary = () => routeErrorComponent() ? CatchBoundary : SafeFragment; const ResolvedNotFoundBoundary = () => routeNotFoundComponent() ? CatchNotFound : SafeFragment; const ShellComponent = route().isRoot ? (route().options.shellComponent ?? SafeFragment) : SafeFragment; return (<ShellComponent> <nearestMatchContext.Provider value={nearestMatch}> <Dynamic component={ResolvedSuspenseBoundary()} fallback={ // Don't show fallback on server when using no-ssr mode to avoid hydration mismatch (isServer ?? router.isServer) && resolvedNoSsr ? undefined : (<Dynamic component={resolvePendingComponent()}/>)}> <Dynamic component={ResolvedCatchBoundary()} getResetKey={() => router.stores.loadedAt.state} errorComponent={routeErrorComponent() || ErrorComponent} onCatch={(error) => { // Forward not found errors (we don't want to show the error component for these) 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}`); } routeOnCatch()?.(error); }}> <Dynamic component={ResolvedNotFoundBoundary()} fallback={(error) => { const notFoundError = getNotFound(error) ?? error; notFoundError.routeId ?? (notFoundError.routeId = currentMatchState() .routeId); // If the current not found handler doesn't exist or it has a // route ID which doesn't match the current route, rethrow the error if (!routeNotFoundComponent() || (notFoundError.routeId && notFoundError.routeId !== currentMatchState().routeId) || (!notFoundError.routeId && !route().isRoot)) throw notFoundError; return (<Dynamic component={routeNotFoundComponent()} {...notFoundError}/>); }}> <Solid.Switch> <Solid.Match when={resolvedNoSsr}> <Solid.Show when={!(isServer ?? router.isServer)} fallback={<Dynamic component={resolvePendingComponent()}/>}> <MatchInner /> </Solid.Show> </Solid.Match> <Solid.Match when={!resolvedNoSsr}> <MatchInner /> </Solid.Match> </Solid.Switch> </Dynamic> </Dynamic> </Dynamic> </nearestMatchContext.Provider> {currentMatchState().parentRouteId === rootRouteId ? (<> <OnRendered /> {router.options.scrollRestoration && (isServer ?? router.isServer) ? (<ScrollRestoration />) : null} </>) : null} </ShellComponent>); }} </Solid.Show>); }; // On Rendered can't happen above the root layout because it needs to run // after the app has committed below the root layout. Keeping it here lets us // fire onRendered even after a hydration mismatch above the root layout // (like bad head/link tags, which is common). function OnRendered() { const router = useRouter(); const location = Solid.createMemo(() => router.stores.resolvedLocation.state?.state.__TSR_key); Solid.createEffect(Solid.on([location], () => { router.emit({ type: 'onRendered', ...getLocationChangeInfo(router.stores.location.state, router.stores.resolvedLocation.state), }); })); return null; } export const MatchInner = () => { const router = useRouter(); const match = Solid.useContext(nearestMatchContext).match; const rawMatchState = Solid.createMemo(() => { const currentMatch = match(); if (!currentMatch) { return null; } const routeId = currentMatch.routeId; const remountFn = router.routesById[routeId].options.remountDeps ?? router.options.defaultRemountDeps; const remountDeps = remountFn?.({ routeId, loaderDeps: currentMatch.loaderDeps, params: currentMatch._strictParams, search: currentMatch._strictSearch, }); const key = remountDeps ? JSON.stringify(remountDeps) : undefined; return { key, routeId, match: { id: currentMatch.id, status: currentMatch.status, error: currentMatch.error, _forcePending: currentMatch._forcePending ?? false, _displayPending: currentMatch._displayPending ?? false, }, }; }); return (<Solid.Show when={rawMatchState()}> {(currentMatchState) => { const route = () => router.routesById[currentMatchState().routeId]; const currentMatch = () => currentMatchState().match; const componentKey = () => currentMatchState().key ?? currentMatchState().match.id; const out = () => { const Comp = route().options.component ?? router.options.defaultComponent; if (Comp) { return <Comp />; } return <Outlet />; }; const keyedOut = () => (<Solid.Show when={componentKey()} keyed> {(_key) => out()} </Solid.Show>); return (<Solid.Switch> <Solid.Match when={currentMatch()._displayPending}> {(_) => { const [displayPendingResult] = Solid.createResource(() => router.getMatch(currentMatch().id)?._nonReactive .displayPendingPromise); return <>{displayPendingResult()}</>; }} </Solid.Match> <Solid.Match when={currentMatch()._forcePending}> {(_) => { const [minPendingResult] = Solid.createResource(() => router.getMatch(currentMatch().id)?._nonReactive .minPendingPromise); return <>{minPendingResult()}</>; }} </Solid.Match> <Solid.Match when={currentMatch().status === 'pending'}> {(_) => { const pendingMinMs = route().options.pendingMinMs ?? router.options.defaultPendingMinMs; if (pendingMinMs) { const routerMatch = router.getMatch(currentMatch().id); if (routerMatch && !routerMatch._nonReactive.minPendingPromise) { // Create a promise that will resolve after the minPendingMs if (!(isServer ?? router.isServer)) { const minPendingPromise = createControlledPromise(); routerMatch._nonReactive.minPendingPromise = minPendingPromise; setTimeout(() => { minPendingPromise.resolve(); // We've handled the minPendingPromise, so we can delete it routerMatch._nonReactive.minPendingPromise = undefined; }, pendingMinMs); } } } const [loaderResult] = Solid.createResource(async () => { await new Promise((r) => setTimeout(r, 0)); return router.getMatch(currentMatch().id)?._nonReactive .loadPromise; }); const FallbackComponent = route().options.pendingComponent ?? router.options.defaultPendingComponent; return (<> {FallbackComponent && pendingMinMs > 0 ? (<Dynamic component={FallbackComponent}/>) : null} {loaderResult()} </>); }} </Solid.Match> <Solid.Match when={currentMatch().status === 'notFound'}> {(_) => { if (!isNotFound(currentMatch().error)) { if (process.env.NODE_ENV !== 'production') { throw new Error('Invariant failed: Expected a notFound error'); } invariant(); } // Use Show with keyed to ensure re-render when routeId changes return (<Solid.Show when={currentMatchState().routeId} keyed> {(_routeId) => renderRouteNotFound(router, route(), currentMatch().error)} </Solid.Show>); }} </Solid.Match> <Solid.Match when={currentMatch().status === 'redirected'}> {(_) => { if (!isRedirect(currentMatch().error)) { if (process.env.NODE_ENV !== 'production') { throw new Error('Invariant failed: Expected a redirect error'); } invariant(); } const [loaderResult] = Solid.createResource(async () => { await new Promise((r) => setTimeout(r, 0)); return router.getMatch(currentMatch().id)?._nonReactive .loadPromise; }); return <>{loaderResult()}</>; }} </Solid.Match> <Solid.Match when={currentMatch().status === 'error'}> {(_) => { if (isServer ?? router.isServer) { const RouteErrorComponent = (route().options.errorComponent ?? router.options.defaultErrorComponent) || ErrorComponent; return (<RouteErrorComponent error={currentMatch().error} info={{ componentStack: '', }}/>); } throw currentMatch().error; }} </Solid.Match> <Solid.Match when={currentMatch().status === 'success'}> {keyedOut()} </Solid.Match> </Solid.Switch>); }} </Solid.Show>); }; export const Outlet = () => { const router = useRouter(); const nearestParentMatch = Solid.useContext(nearestMatchContext); const parentMatch = nearestParentMatch.match; const routeId = nearestParentMatch.routeId; const route = Solid.createMemo(() => routeId() ? router.routesById[routeId()] : undefined); const parentGlobalNotFound = Solid.createMemo(() => parentMatch()?.globalNotFound ?? false); const childMatchId = Solid.createMemo(() => { const currentRouteId = routeId(); return currentRouteId ? router.stores.childMatchIdByRouteId.state[currentRouteId] : undefined; }); const childMatchStatus = Solid.createMemo(() => { const id = childMatchId(); if (!id) return undefined; return router.stores.activeMatchStoresById.get(id)?.state.status; }); // Only show not-found if we're not in a redirected state const shouldShowNotFound = () => childMatchStatus() !== 'redirected' && parentGlobalNotFound(); return (<Solid.Show when={!shouldShowNotFound() && childMatchId()} fallback={<Solid.Show when={shouldShowNotFound() && route()}> {(resolvedRoute) => renderRouteNotFound(router, resolvedRoute(), undefined)} </Solid.Show>}> {(childMatchIdAccessor) => { const currentMatchId = Solid.createMemo(() => childMatchIdAccessor()); return (<Solid.Show when={routeId() === rootRouteId} fallback={<Match matchId={currentMatchId()}/>}> <Solid.Suspense fallback={<Dynamic component={router.options.defaultPendingComponent}/>}> <Match matchId={currentMatchId()}/> </Solid.Suspense> </Solid.Show>); }} </Solid.Show>); }; //# sourceMappingURL=Match.jsx.map