@tanstack/solid-router
Version:
Modern and scalable routing for Solid applications
166 lines (165 loc) • 6.92 kB
JavaScript
import { renderInNonRouteComponentContext } from "./nonRouteComponentContext.js";
import { CatchBoundary, ErrorComponent } from "./CatchBoundary.js";
import { useRouter } from "./useRouter.js";
import { nearestMatchContext } from "./matchContext.js";
import { Rendered, Transitioner } from "./Transitioner.js";
import { SafeFragment } from "./SafeFragment.js";
import { Match } from "./Match.js";
import { replaceEqualDeep, rootRouteId } from "@tanstack/router-core";
import { createComponent, memo } from "@solidjs/web";
import * as Solid from "solid-js";
//#region src/Matches.tsx
var NearestMatchContext = nearestMatchContext;
/**
* Select the global loading boundary for `Matches`.
*
* Loading UI belongs to the application, not the router. Solid 2's async
* model never injects fallbacks the app didn't write: pending state
* propagates through the graph to whatever boundary the app placed (or is
* held at the root when there is none), and an unresolved `lazy()` chunk is
* just a pending async value. The wrapper here is therefore strictly opt-in:
* it renders only when the app configured root pending UI
* (`pendingComponent` on the root route or `defaultPendingComponent`).
* Nothing configured means no wrapper — pending match/chunk states propagate
* as ordinary Solid async.
*
* The decision is deliberately SYMMETRIC between server and client, and
* consults no hydration or environment state. Solid's hydration claims
* server nodes positionally through the boundary structure, so the client
* must render a boundary exactly where the server rendered one — a
* client-only wrapper (even a settled one that renders its children
* directly) desyncs node claiming and detaches the app from the server DOM.
* The inputs below (`pendingComponent` configuration,
* `disableGlobalCatchBoundary`, `router.ssr`) all resolve identically on the
* server and on the hydrating client, so the trees always agree. This also
* keeps the decision stable across the app's lifetime: it is made once per
* `Matches` instance, so gating it on transient state (like "currently
* hydrating") would permanently freeze the configured pending UI off for
* every post-hydration navigation of a hydrated app.
*
* `router.ssr` (TanStack's `$_TSR` SSR utilities) is symmetric too:
* `attachRouterServerSsrUtils` sets it on the server before rendering, and
* router-core `hydrate()` (invoked by `RouterClient`, which TanStack Start
* builds on) sets it on the client before rendering. It short-circuits the
* wrapper exactly as before: that protocol legitimately hydrates matches in
* pending states (`ssr: 'data-only'`), where a settled boundary is not
* guaranteed.
*
* When disableGlobalCatchBoundary is true, we must NOT wrap with
* Solid.Loading because Solid.Loading transforms STATUS_ERROR into
* STATUS_PENDING, which prevents errors from propagating to an external
* Errored boundary.
*/
function _resolveMatchesLoadingBoundary(router) {
return !(router.routesById[rootRouteId]?.options.pendingComponent ?? router.options.defaultPendingComponent) || router.options.disableGlobalCatchBoundary || router.ssr ? SafeFragment : Solid.Loading;
}
function Matches() {
const router = useRouter();
const ResolvedSuspense = _resolveMatchesLoadingBoundary(router);
const rootRoute = () => router.routesById[rootRouteId];
const PendingComponent = rootRoute().options.pendingComponent ?? router.options.defaultPendingComponent;
return createComponent(router.options.InnerWrap || SafeFragment, { get children() {
return createComponent(ResolvedSuspense, {
get fallback() {
return PendingComponent ? (() => {
if (process.env.NODE_ENV !== "production") return renderInNonRouteComponentContext(() => createComponent(PendingComponent, {}), "pendingComponent");
return createComponent(PendingComponent, {});
})() : null;
},
get children() {
return [
createComponent(Transitioner, {}),
createComponent(MatchesInner, {}),
createComponent(Rendered, {})
];
}
});
} });
}
function MatchesInner() {
const router = useRouter();
const routeId = () => router.stores.ids.get()[0];
const match = () => routeId() ? router.stores.byRoute.get(routeId())?.get() : void 0;
const nearestMatch = [routeId, match];
const matchContent = () => createComponent(Solid.Show, {
get when() {
return routeId();
},
keyed: true,
children: (currentRouteId) => createComponent(Match, { routeId: currentRouteId })
});
if (router.options.disableGlobalCatchBoundary) return createComponent(NearestMatchContext, {
value: nearestMatch,
get children() {
return matchContent();
}
});
return createComponent(NearestMatchContext, {
value: nearestMatch,
get children() {
return createComponent(CatchBoundary, {
getResetKey: () => router.stores.matches.get(),
render: matchContent,
errorComponent: ErrorComponent,
get onCatch() {
return process.env.NODE_ENV !== "production" ? (error) => {
console.warn(`Warning: The following error wasn't caught by any route! At the very least, consider setting an 'errorComponent' in your RootRoute!`);
console.warn(`Warning: ${error.message || error.toString()}`);
} : void 0;
}
});
}
});
}
function useMatchRoute() {
const router = useRouter();
return (opts) => {
return Solid.createMemo(() => {
const { pending, caseSensitive, fuzzy, includeSearch, ...rest } = opts;
router.stores.location.get();
router.stores.resolvedLocation.get();
router.stores.status.get();
return router.matchRoute(rest, {
pending,
caseSensitive,
fuzzy,
includeSearch
});
});
};
}
function MatchRoute(props) {
const params = useMatchRoute()(props);
return memo(Solid.createMemo(() => {
const matchedParams = params();
const child = props.children;
if (typeof child === "function") return child(matchedParams);
return matchedParams ? child : null;
}));
}
function useMatches(opts) {
const router = useRouter();
return Solid.createMemo((prev) => {
const matches = router.stores.matches.get();
const res = opts?.select ? opts.select(matches) : matches;
if (prev === void 0) return res;
return replaceEqualDeep(prev, res);
});
}
function useParentMatches(opts) {
const contextRouteId = Solid.useContext(nearestMatchContext)[0];
return useMatches({ select: (matches) => {
matches = matches.slice(0, matches.findIndex((d) => d.routeId === contextRouteId()));
return opts?.select ? opts.select(matches) : matches;
} });
}
function useChildMatches(opts) {
const contextRouteId = Solid.useContext(nearestMatchContext)[0];
return useMatches({ select: (matches) => {
matches = matches.slice(matches.findIndex((d) => d.routeId === contextRouteId()) + 1);
return opts?.select ? opts.select(matches) : matches;
} });
}
//#endregion
export { MatchRoute, Matches, useChildMatches, useMatchRoute, useMatches, useParentMatches };
//# sourceMappingURL=Matches.js.map