UNPKG

@tanstack/solid-router

Version:

Modern and scalable routing for Solid applications

349 lines (348 loc) 17.9 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 '@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'; const NearestMatchContext = nearestMatchContext; 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 = Solid.createMemo(() => router.routesById[currentMatchState().routeId]); const resolvePendingComponent = Solid.createMemo(() => route().options.pendingComponent ?? router.options.defaultPendingComponent); const routeErrorComponent = Solid.createMemo(() => route().options.errorComponent ?? router.options.defaultErrorComponent); const routeOnCatch = Solid.createMemo(() => route().options.onCatch ?? router.options.defaultOnCatch); const routeNotFoundComponent = Solid.createMemo(() => 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 = Solid.createMemo(() => currentMatchState().ssr === false || currentMatchState().ssr === 'data-only'); const ResolvedSuspenseBoundary = Solid.createMemo(() => resolvedNoSsr() ? SafeFragment : Solid.Loading); const ResolvedCatchBoundary = Solid.createMemo(() => routeErrorComponent() ? CatchBoundary : SafeFragment); const ResolvedNotFoundBoundary = Solid.createMemo(() => routeNotFoundComponent() ? CatchNotFound : SafeFragment); const ShellComponent = Solid.createMemo(() => route().isRoot ? (route().options.shellComponent ?? SafeFragment) : SafeFragment); return (<Dynamic component={ShellComponent()}> <NearestMatchContext 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> {currentMatchState().parentRouteId === rootRouteId ? (<> <OnRendered /> {router.options.scrollRestoration && (isServer ?? router.isServer) ? (<ScrollRestoration />) : null} </>) : null} </Dynamic>); }} </Solid.Show>); }; // On Rendered can't happen above the root layout because it actually // renders a dummy dom element to track the rendered state of the app. // We render a script tag with a key that changes based on the current // location state.__TSR_key. Also, because it's below the root layout, it // allows us to fire onRendered events even after a hydration mismatch // error that occurred above the root layout (like bad head/link tags, // which is common). // // In Solid, createEffect(source, fn) fires on initial mount as well as on // reactive changes. OnRendered can also remount when the first child route // changes (e.g. navigating from / to /posts). We deduplicate by tracking // the last emitted resolvedLocation key per router so each unique resolved // location only triggers one onRendered event regardless of remounts. const lastOnRenderedKey = new WeakMap(); function OnRendered() { const router = useRouter(); const location = Solid.createMemo(() => router.stores.resolvedLocation.state?.state.__TSR_key); const locationState = Solid.createMemo(() => router.stores.location.state); const resolvedLocationState = Solid.createMemo(() => router.stores.resolvedLocation.state); Solid.createEffect(() => [location(), locationState(), resolvedLocationState()], ([location, currentLocationState, currentResolvedLocationState]) => { if (!location) return; if (lastOnRenderedKey.get(router) === location) return; lastOnRenderedKey.set(router, location); router.emit({ type: 'onRendered', ...getLocationChangeInfo(currentLocationState, currentResolvedLocationState), }); }); 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 = Solid.createMemo(() => router.routesById[currentMatchState().routeId]); const currentMatch = Solid.createMemo(() => currentMatchState().match); const componentKey = Solid.createMemo(() => currentMatchState().key ?? currentMatchState().match.id); const Comp = Solid.createMemo(() => route().options.component ?? router.options.defaultComponent); const OutComponent = Solid.createMemo(() => { const C = Comp(); return C || Outlet; }); const RenderOut = () => <Dynamic component={OutComponent()}/>; const keyedOut = () => (<Solid.Show when={componentKey()} keyed> {(_key) => <RenderOut />} </Solid.Show>); return (<Solid.Switch> <Solid.Match when={currentMatch()._displayPending}> {(_) => { const displayPendingResult = Solid.createMemo(() => router.getMatch(currentMatch().id)?._nonReactive .displayPendingPromise); return <>{displayPendingResult()}</>; }} </Solid.Match> <Solid.Match when={currentMatch()._forcePending}> {(_) => { const minPendingResult = Solid.createMemo(() => router.getMatch(currentMatch().id)?._nonReactive .minPendingPromise); return <>{minPendingResult()}</>; }} </Solid.Match> <Solid.Match when={currentMatch().status === 'pending'}> {(_) => { const pendingMinMs = Solid.untrack(() => route().options.pendingMinMs ?? router.options.defaultPendingMinMs); if (pendingMinMs) { const routerMatch = Solid.untrack(() => 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.createMemo(async () => { await new Promise((r) => setTimeout(r, 0)); return router.getMatch(currentMatch().id)?._nonReactive .loadPromise; }); const FallbackComponent = Solid.untrack(() => route().options.pendingComponent ?? router.options.defaultPendingComponent); return (<> {FallbackComponent && pendingMinMs > 0 ? (<Dynamic component={FallbackComponent}/>) : null} {loaderResult()} </>); }} </Solid.Match> <Solid.Match when={currentMatch().status === 'notFound'}> {(_) => { const matchError = Solid.untrack(() => currentMatch().error); if (!isNotFound(matchError)) { 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) => Solid.untrack(() => renderRouteNotFound(router, route(), matchError))} </Solid.Show>); }} </Solid.Match> <Solid.Match when={currentMatch().status === 'redirected'}> {(_) => { const matchError = Solid.untrack(() => currentMatch().error); if (!isRedirect(matchError)) { if (process.env.NODE_ENV !== 'production') { throw new Error('Invariant failed: Expected a redirect error'); } invariant(); } return null; }} </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 (<RouteErrorComponent error={matchError} info={{ componentStack: '', }}/>); } throw matchError; }} </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 childRouteId = Solid.createMemo(() => { const id = childMatchId(); if (!id) return undefined; return router.stores.activeMatchStoresById.get(id)?.state.routeId; }); const childRoute = Solid.createMemo(() => { const id = childRouteId(); return id ? router.routesById[id] : undefined; }); const childPendingComponent = Solid.createMemo(() => childRoute()?.options.pendingComponent ?? router.options.defaultPendingComponent); 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) => Solid.untrack(() => renderRouteNotFound(router, resolvedRoute(), undefined))} </Solid.Show>}> {(childMatchIdAccessor) => { const currentMatchId = Solid.createMemo(() => childMatchIdAccessor()); return (<Solid.Show when={routeId() === rootRouteId} fallback={<Match matchId={currentMatchId()}/>}> <Solid.Show when={childRouteId()} keyed> {(_routeId) => (<Solid.Loading fallback={childPendingComponent() ? (<Dynamic component={childPendingComponent()}/>) : null}> <Match matchId={currentMatchId()}/> </Solid.Loading>)} </Solid.Show> </Solid.Show>); }} </Solid.Show>); }; //# sourceMappingURL=Match.jsx.map