@tanstack/solid-router
Version:
Modern and scalable routing for Solid applications
293 lines (292 loc) • 13.3 kB
JSX
import * as Solid from 'solid-js';
import invariant from 'tiny-invariant';
import warning from 'tiny-warning';
import { createControlledPromise, getLocationChangeInfo, 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 { useRouterState } from './useRouterState';
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';
export const Match = (props) => {
const router = useRouter();
const matchState = useRouterState({
select: (s) => {
const match = s.matches.find((d) => d.id === props.matchId);
// During navigation transitions, matches can be temporarily removed
// Return null to avoid errors - the component will handle this gracefully
if (!match) {
return null;
}
return {
routeId: match.routeId,
ssr: match.ssr,
_displayPending: match._displayPending,
};
},
});
// If match doesn't exist yet, return null (component is being unmounted or not ready)
if (!matchState())
return null;
const route = () => router.routesById[matchState().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 = matchState().ssr === false || matchState().ssr === 'data-only';
const ResolvedSuspenseBoundary = () => Solid.Suspense;
const ResolvedCatchBoundary = () => routeErrorComponent() ? CatchBoundary : SafeFragment;
const ResolvedNotFoundBoundary = () => routeNotFoundComponent() ? CatchNotFound : SafeFragment;
const resetKey = useRouterState({
select: (s) => s.loadedAt,
});
const parentRouteId = useRouterState({
select: (s) => {
const index = s.matches.findIndex((d) => d.id === props.matchId);
return s.matches[index - 1]?.routeId;
},
});
const ShellComponent = route().isRoot
? (route().options.shellComponent ?? SafeFragment)
: SafeFragment;
return (<ShellComponent>
<matchContext.Provider value={() => props.matchId}>
<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={() => resetKey()} errorComponent={routeErrorComponent() || ErrorComponent} onCatch={(error) => {
// Forward not found errors (we don't want to show the error component for these)
if (isNotFound(error))
throw error;
warning(false, `Error in route match: ${matchState().routeId}`);
routeOnCatch()?.(error);
}}>
<Dynamic component={ResolvedNotFoundBoundary()} fallback={(error) => {
// 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() ||
(error.routeId && error.routeId !== matchState().routeId) ||
(!error.routeId && !route().isRoot))
throw error;
return (<Dynamic component={routeNotFoundComponent()} {...error}/>);
}}>
<Solid.Switch>
<Solid.Match when={resolvedNoSsr}>
<Solid.Show when={!(isServer ?? router.isServer)} fallback={<Dynamic component={resolvePendingComponent()}/>}>
<MatchInner matchId={props.matchId}/>
</Solid.Show>
</Solid.Match>
<Solid.Match when={!resolvedNoSsr}>
<MatchInner matchId={props.matchId}/>
</Solid.Match>
</Solid.Switch>
</Dynamic>
</Dynamic>
</Dynamic>
</matchContext.Provider>
{parentRouteId() === rootRouteId ? (<>
<OnRendered />
<ScrollRestoration />
</>) : null}
</ShellComponent>);
};
// 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).
function OnRendered() {
const router = useRouter();
const location = useRouterState({
select: (s) => {
return s.resolvedLocation?.state.__TSR_key;
},
});
Solid.createEffect(Solid.on([location], () => {
router.emit({
type: 'onRendered',
...getLocationChangeInfo(router.state),
});
}));
return null;
}
export const MatchInner = (props) => {
const router = useRouter();
const matchState = useRouterState({
select: (s) => {
const match = s.matches.find((d) => d.id === props.matchId);
// During navigation transitions, matches can be temporarily removed
if (!match) {
return null;
}
const routeId = match.routeId;
const remountFn = router.routesById[routeId].options.remountDeps ??
router.options.defaultRemountDeps;
const remountDeps = remountFn?.({
routeId,
loaderDeps: match.loaderDeps,
params: match._strictParams,
search: match._strictSearch,
});
const key = remountDeps ? JSON.stringify(remountDeps) : undefined;
return {
key,
routeId,
match: {
id: match.id,
status: match.status,
error: match.error,
_forcePending: match._forcePending,
_displayPending: match._displayPending,
},
};
},
});
if (!matchState())
return null;
const route = () => router.routesById[matchState().routeId];
const match = () => matchState().match;
const componentKey = () => matchState().key ?? matchState().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={match()._displayPending}>
{(_) => {
const [displayPendingResult] = Solid.createResource(() => router.getMatch(match().id)?._nonReactive.displayPendingPromise);
return <>{displayPendingResult()}</>;
}}
</Solid.Match>
<Solid.Match when={match()._forcePending}>
{(_) => {
const [minPendingResult] = Solid.createResource(() => router.getMatch(match().id)?._nonReactive.minPendingPromise);
return <>{minPendingResult()}</>;
}}
</Solid.Match>
<Solid.Match when={match().status === 'pending'}>
{(_) => {
const pendingMinMs = route().options.pendingMinMs ?? router.options.defaultPendingMinMs;
if (pendingMinMs) {
const routerMatch = router.getMatch(match().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(match().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={match().status === 'notFound'}>
{(_) => {
invariant(isNotFound(match().error), 'Expected a notFound error');
// Use Show with keyed to ensure re-render when routeId changes
return (<Solid.Show when={matchState().routeId} keyed>
{(_routeId) => renderRouteNotFound(router, route(), match().error)}
</Solid.Show>);
}}
</Solid.Match>
<Solid.Match when={match().status === 'redirected'}>
{(_) => {
invariant(isRedirect(match().error), 'Expected a redirect error');
const [loaderResult] = Solid.createResource(async () => {
await new Promise((r) => setTimeout(r, 0));
return router.getMatch(match().id)?._nonReactive.loadPromise;
});
return <>{loaderResult()}</>;
}}
</Solid.Match>
<Solid.Match when={match().status === 'error'}>
{(_) => {
throw match().error;
}}
</Solid.Match>
<Solid.Match when={match().status === 'success'}>
{keyedOut()}
</Solid.Match>
</Solid.Switch>);
};
export const Outlet = () => {
const router = useRouter();
const matchId = Solid.useContext(matchContext);
const routeId = useRouterState({
select: (s) => s.matches.find((d) => d.id === matchId())?.routeId,
});
const route = () => router.routesById[routeId()];
const parentGlobalNotFound = useRouterState({
select: (s) => {
const matches = s.matches;
const parentMatch = matches.find((d) => d.id === matchId());
// During navigation transitions, parent match can be temporarily removed
// Return false to avoid errors - the component will handle this gracefully
if (!parentMatch) {
return false;
}
return parentMatch.globalNotFound;
},
});
const childMatchId = useRouterState({
select: (s) => {
const matches = s.matches;
const index = matches.findIndex((d) => d.id === matchId());
const v = matches[index + 1]?.id;
return v;
},
});
const childMatchStatus = useRouterState({
select: (s) => {
const matches = s.matches;
const index = matches.findIndex((d) => d.id === matchId());
return matches[index + 1]?.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()}>
{renderRouteNotFound(router, route(), undefined)}
</Solid.Show>}>
{(matchIdAccessor) => {
// Use a memo to avoid stale accessor errors while keeping reactivity
const currentMatchId = Solid.createMemo(() => matchIdAccessor());
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