Switch & Not Found Pages
A page built from several <Route>s side by side renders every one that matches - useful when
routes genuinely overlap, but usually not what you want for a set of alternatives. Switch
renders only the first matching child, and a fallback for when none does.
import { Route, Switch } from "jarl-react";
import { aboutRoute, usersRoute, userRoute } from "./routes";
export default () => (
<Switch fallback={<NotFound />}>
<Route on={aboutRoute} exact>
<AboutPage />
</Route>
<Route on={usersRoute} exact>
<UsersPage />
</Route>
<Route on={userRoute} exact>
{({ id }) => <UserPage id={id} />}
</Route>
</Switch>
);Children are judged in order, each by its own exact prop - the first <Route> whose on
matches (exactly, if it says exact) wins, and the rest don't render even if they'd have
matched too. Switch reads its children's props rather than rendering them to find out, so they
must be literal <Route> elements: a fragment or wrapper around a group of them hides those
routes from Switch, and throws rather than silently dropping them. A conditional child is fine -
{showAdmin && <Route on={adminRoute}>...</Route>} - Switch just skips a falsy one.
Nest a Switch inside a <Route> for a fallback scoped to that branch, rather than the whole
app falling back to the same not-found page regardless of where in the tree the miss happened:
<Switch fallback={<NotFound />}>
<Route on={usersRoute}>
<Switch fallback={<UserNotFound />}>
<Route on={usersRoute} exact>
<UsersIndex />
</Route>
<Route on={userRoute} exact>
{({ id }) => <UserPage id={id} />}
</Route>
</Switch>
</Route>
</Switch>/users/42/settings falls back to <UserNotFound /> from the inner Switch; only a path outside
usersRoute entirely reaches the outer one. Note the outer <Route on={usersRoute}> has no
exact: it's there to select the branch, not to render a leaf.
notAtom: not-found as a value, not a render
Switch's fallback solves what to render when nothing matches. Sometimes you also need the
answer as plain data - most commonly a real HTTP status code from a server render, which has to
exist before there's any HTML to talk about. notAtom is a boolean atom: true when none of the
given route atoms is an exact match.
// routes.ts
export const notFoundAtom = notAtom(homeRoute, aboutRoute, usersRoute, userRoute);// entry-server.tsx
const status = store.get(notFoundAtom) ? 404 : 200;List every route your app actually renders, the same way Switch's children would be listed.
notAtom checks exact rather than match: an ancestor route (usersRoute, or rootRoute
itself) is match: true for everything beneath it, so checking match would mean notAtom never
fires. For an async route, list the async atom itself (asyncRouteAtom's return value) rather
than its underlying param route - an unresolved lookup is a genuine miss even though the param
route it wraps still matched syntactically. See the Data Loading guide for
how that interacts with preloadRoutes.
Nested routers and the status code
A route atom scoped under its own basePath (see
Location & Base Paths) routes a whole subtree that the outer route
table knows nothing about. Listing its mount point in notAtom won't do: the mount point is only
exact for the subtree's index page, so every deeper URL inside it would report a 404 while the
subtree happily renders a page.
notAtom is a plain jotai atom, so compose over it rather than working around it - exempt each
nested router's whole subtree by its match, and let the subtree answer for itself:
const exactRouteMissedAtom = notAtom(homeRoute, aboutRoute, usersRoute, userRoute, blogMountRoute);
export const notFoundAtom = atom((get) => get(exactRouteMissedAtom) && !get(blogMountRoute).match);That's a deliberate trade: anything under the nested router's mount returns a 200 even when the
subtree renders its own not-found view. Where the answer genuinely matters to a crawler, keep the
routes in the outer table instead - or, when the miss is a lookup rather than a URL shape, use an
asyncRouteAtom, which notAtom can judge exactly.