API Reference: jarl-atoms
The core exports of jarl-atoms - the framework-agnostic half of JARL. Everything here is a
plain jotai atom with no React dependency; the React components and hooks
that consume these atoms live in the sibling jarl-react package. See the
v1 History page for how JARL's original RouteMap/RoutingProvider API worked, and
why the atomic model replaced it.
Every route atom - whatever kind - shares the same shape when read: { match, exact, values, reverse, rest? }. match/exact tell you whether (and how completely) it matches the current
location, values gives you the params it (and its ancestors) bound, and reverse(values)
turns a set of param values back into a URL. Writing to a route atom navigates.
The reference below is generated from the doc comments on each export.
Functions and atoms
locationAtom
const locationAtom: WritableAtom<JarlLocation, [SetStateAction<JarlLocation>, ({ replace?: boolean; } | undefined)?], void>The location every route atom reads from, and the seam where SSR/SSG is made possible.
In a browser reads and writes go straight through to jotai-location, so navigation
still drives real history.pushState/replaceState, and every same-document
navigation - including one made from outside jarl - is picked up. A write is subject
to any guard registered with enforceNavigationGuards, and does nothing if one blocks it.
Under Node there is no window to push history onto, so writes are captured
in plain jotai state instead and reads prefer that captured value. That makes
a route seedable per-render on the server:
const store = createStore();
store.set(locationAtom, { pathname: "/docs", searchParams: new URLSearchParams() });
renderToString(<Provider store={store}><App /></Provider>);Each store keeps its own override, so prerendering many routes in one process can't leak location between them.
routeAtom
const routeAtom: <T extends DefaultParams = DefaultParams, Parent extends DefaultParams = DefaultParams>(matchPath: (path: string, get: Getter) => T | undefined, makePath: (values: T, get: Getter) => string, options?: RouteOptions<Parent>) => RouteAtom<T & Parent>The primitive every other route atom is built from. matchPath decides whether the next
unconsumed path segment matches, and to what param values; makePath is its inverse, used by
reverse. Reach for it directly when staticRouteAtom/paramRouteAtom don't fit - custom
segment syntax, regex constraints and the like.
rootRouteAtom
const rootRouteAtom: (options?: RootRouteOptions) => RouteAtom<DefaultParams>Creates a root RouteAtom. Call this instead of using the default rootRoute export when the
app needs to be scoped under a basePath - e.g. mounted under a subpath, or as one router
among several sharing a page. The returned atom is a normal RouteAtom: pass it as parent
to the routes built on top of it, exactly like rootRoute itself.
rootRoute
const rootRoute: RouteAtom<DefaultParams>The default root of every route atom chain: matches /, and is the implicit parent.
staticRouteAtom
const staticRouteAtom: <Parent extends DefaultParams>(name: string, options?: RouteOptions<Parent>) => RouteAtom<Parent>Matches one fixed path segment: staticRouteAtom("about") matches /about. Binds no values
of its own - values is just whatever parent bound - so it's the atom to reach for whenever
a step in the path doesn't need to capture anything.
paramRouteAtom
const paramRouteAtom: <T extends string, Parent extends DefaultParams>(name: T, options?: RouteOptions<Parent>) => RouteAtom<{ [key in T]: string; } & Parent>Binds one dynamic path segment to a named value: paramRouteAtom("productId", { parent: products }) matches /products/:productId and yields { productId: "123" }.
numericRouteAtom
const numericRouteAtom: <T extends string, Parent extends DefaultParams>(name: T, options?: NumericRouteOptions<Parent>) => RouteAtom<{ [key in T]: number; } & Parent>Binds one path segment to a named non-negative integer, typed as a number rather than as a string:
const yearRoute = numericRouteAtom("year", { parent: blogRoute, min: 2000 });A segment that isn't all digits leaves the route unmatched, as does one outside the inclusive
min/max range.
enumRouteAtom
const enumRouteAtom: <Name extends string, const Values extends EnumValues, Parent extends DefaultParams>(name: Name, allowed: Values, options?: RouteOptions<Parent>) => RouteAtom<{ [key in Name]: Values[number]; } & Parent>Binds one path segment to a named value from a fixed set, typed as the union of those values
rather than as string:
const pageRoute = enumRouteAtom("page", ["home", "about", "contact"], { parent: siteRoute });Only those segments match; any other leaves the route unmatched. Writing, reversing or linking with a value outside the set is a compile error.
Pass a literal array, or an as const tuple to share the list with the code that consumes it.
A plain string[] has no literals left to bind and won't type.
transformRouteAtom
const transformRouteAtom: <T extends DefaultParams, Return extends DefaultParams>(parentAtom: RouteAtom<T>, getter: (values: T, get: Getter) => Return | undefined, setter: (values: Return, get: Getter) => T) => RouteAtom<Return>Reshapes a route's matched values into a different shape, and back again for reverse and
writes.
getter runs only when parentAtom matches. Returning undefined leaves this atom unmatched
too, which is how a segment the parent accepted gets rejected.
setter is its inverse, and must produce values parentAtom itself accepts.
validateRouteAtom
const validateRouteAtom: <T extends DefaultParams>(parentAtom: RouteAtom<T>, isValid: (values: T, get: Getter) => boolean) => RouteAtom<T>Narrows a route to the values a predicate accepts, for a constraint spanning several segments that no single segment's own options can express:
const dateRoute = validateRouteAtom(dayRoute, ({ year, month, day }) =>
isValidCalendarDate(year, month, day),
);Values the predicate rejects leave the route unmatched, so 31 February falls through to whatever handles a non-matching URL.
The predicate is also given a Getter, so it can validate against other atoms.
unionRouteAtom
const unionRouteAtom: <Routes extends UnionRoutes>(routes: Routes) => RouteAtom<RouteValues<Routes[number]>>Combines several routes into the one route that matches wherever any of them does: it matches
when any member matches, and exactly when any member matches exactly. values, rest and
reverse come from the member that actually matched - the exact one where there is one,
otherwise the first to match, in the order given - so a chain of increasingly specific routes
reads as one route bound to a union of their param types.
It composes like any other route: as a parent, in <Route on={...}>, or as the whole route
list handed to notAtom. A union has no URL shape of its own, though, so reverse and writes
go to whichever member matches, and to the first listed when none does - navigate through the
specific member you mean rather than through the union.
notAtom
const notAtom: (route: RouteAtom<any>, { exact }?: NotOptions) => Atom<boolean>Matches when route doesn't - the inverse of a router's full route list, for a
catch-all/not-found case. Combine every route the app renders into one unionRouteAtom to
negate the lot: notAtom(unionRouteAtom([homeRoute, postRoute, ...])).
Exactness is what counts by default: an ancestor route (or rootRoute itself) can be
match: true without being the leaf that actually rendered, and only the leaf's exactness
should decide whether anything was found. Pass { exact: false } where an ancestor match
should count too.
normalizePathname
const normalizePathname: (pathname: string) => stringNormalizes a pathname to a leading slash, no repeated slashes and no trailing slash.
splitHref
const splitHref: (href: Path) => [pathname: string, searchParams: URLSearchParams]Splits a full href (e.g. /foo/bar?a=1&b=2) into a normalized pathname
and a URLSearchParams instance for the query string.
appendQueryParam
const appendQueryParam: (href: Path, key: string, value: string | undefined) => PathAppends/overwrites a single query param onto an existing href, returning the combined
href. Passing undefined as the value removes the param.
joinHref
const joinHref: (pathname: string, searchParams: URLSearchParams) => PathJoins a pathname and a URLSearchParams back into a single href string.
parseQuery
const parseQuery: (search: URLSearchParams | string) => Record<string, string | string[]>Parses a URLSearchParams (or query string) into a plain object. Repeated
keys become string arrays, matching the common (non-qs) convention.
stringifyQuery
const stringifyQuery: (query: Record<string, string | string[] | undefined>) => stringInverse of parseQuery: serializes a plain object into a query string
(without the leading ?).
queryAtom
const queryAtom: WritableAtom<Record<string, string | string[]>, [query: Record<string, string | string[] | undefined>, navOptions?: NavOptions | undefined], void>Read/write atom for the whole current query string, as a plain object.
Reading never fails to match; writing replaces the entire query string
(pass undefined for a key to remove it, keep other current keys by
spreading get(queryAtom) yourself first).
queryParamRouteAtom
const queryParamRouteAtom: <T extends string, Parent extends DefaultParams = DefaultParams>(name: T, options?: QueryParamRouteOptions<Parent>) => RouteAtom<{ readonly [key in T]: string | undefined; } & Parent>Binds one named query param, composing exactly like a path route atom:
const searchRoute = queryParamRouteAtom("q", { parent: searchPageRoute });parent may be any route atom, path- or query-based. reverse and writes round-trip through
the same href as that parent, with this param appended or updated on top.
Consumes no path segments, so path matching is unaffected by however many query params are chained on.
redirect
const redirect: (to: Path) => RedirectConstructs a Redirect sentinel object - typically returned from an asyncRouteAtom loader to
defer a redirect decision until after data has loaded.
isRedirect
const isRedirect: (value: unknown) => value is RedirectType guard for a Redirect sentinel, e.g. narrowing a resolver's return value before acting on it.
redirectRouteAtom
const redirectRouteAtom: <Parent extends DefaultParams = DefaultParams>(to: Path | ((get: Getter) => Path), options?: RouteOptions<Parent>) => RouteAtom<Parent>A leaf route that matches whenever its parent does, swallowing any remaining path, and whose
reverse()/write resolve to the redirect target rather than to itself. to may be a static
path or a function of get, for a target computed from other atoms.
Reading it is pure: matching one navigates nowhere on its own, see followRedirects.
followRedirects
const followRedirects: (store: Store, redirectRouteAtoms: ReadonlyArray<RouteAtom<any>>) => (() => void)Makes redirect atoms actually navigate: subscribes to each, and replace-navigates the moment one starts matching. Call once, near the root of an app, for every redirect atom you want live. Returns an unsubscribe function.
asyncRouteAtom
const asyncRouteAtom: <T extends DefaultParams, Name extends string, Data>(parentAtom: RouteAtom<T>, name: Name, load: RouteLoader<T, Data>) => AsyncRouteAtom<T, Name, Data>Attaches an async load to a route, answering both "what data does this route have" and, if you
want it to, "does this route exist at all". load runs against the parent route's matched
values whenever the parent matches.
Read .data and that is all it is - a plain async atom of the loaded value, alongside a route
that already matched, consumed however suits: useAtomValue under Suspense, jotai/utils
loadable(), or await store.get(...) outside React. Nothing else needs wiring up.
const productData = asyncRouteAtom(productRoute, "product", ({ id }) => api.product(id)).data;Read the atom itself as a route and the load decides the match: undefined back means this
route doesn't match, so a Switch fallback or notAtom renders the not-found case, and
anything else matches with that object bound to name in the route's own values.
const postRoute = asyncRouteAtom(slugRoute, "post", ({ slug }) => db.findPost(slug));
// <Route on={postRoute} exact>{({ post }) => <PostView post={post} />}</Route>Matching stays synchronous, so that second use needs the load settled into the store first:
await preloadAsyncRoutes(store, routes) before a server render, hydrateAsyncRoutes to seed
the client with what that render already loaded, and followAsyncRoutes once at startup to
keep it settled across client navigation.
preloadAsyncRoutes
const preloadAsyncRoutes: (store: Store, routes: ReadonlyArray<AsyncRouteAtom<any, any, any>>) => Promise<AsyncRouteSnapshot[]>Awaits each route's load for the store's current location and settles it, so a render that
follows sees the routes match, or not, synchronously - the step a server render needs before
it can produce HTML and a status code. Returns the snapshots, in the order given, to serialise
into the page for hydrateAsyncRoutes. Routes already settled for that location aren't loaded
again.
hydrateAsyncRoutes
const hydrateAsyncRoutes: (store: Store, routes: ReadonlyArray<AsyncRouteAtom<any, any, any>>, snapshots: ReadonlyArray<AsyncRouteSnapshot>) => voidSeeds a server render's snapshots into a client store, so hydration matches the HTML it
received without loading anything a second time. snapshots are matched to routes by
position, exactly as preloadAsyncRoutes returned them.
followAsyncRoutes
const followAsyncRoutes: (store: Store, routes: ReadonlyArray<AsyncRouteAtom<any, any, any>>) => (() => void)Keeps async routes settled as the location changes: re-runs each load on navigation and
publishes what it finds. Call once near the root of a client app, after hydrateAsyncRoutes.
Returns an unsubscribe function.
followAsyncRedirects
const followAsyncRedirects: (store: Store, dataAtoms: ReadonlyArray<Atom<Promise<unknown>>>) => (() => void)Follows any Redirect a loader produces, replace-navigating to its target - the async-loading
counterpart of followRedirects. Takes the data atoms to watch. Returns an unsubscribe
function.
navigationGuardAtom
const navigationGuardAtom: (guard: (get: Getter) => string | null) => NavigationGuardAtomA guard that vetoes navigations while its condition holds - typically an unsaved-edits prompt:
const unsavedGuard = navigationGuardAtom((get) =>
get(formDirtyAtom) ? "You have unsaved edits. Leave anyway?" : null,
);Returning a string blocks the navigation behind a window.confirm carrying that message;
returning null allows it. Reading one is pure - enforceNavigationGuards is the effect that
makes it bite, and documents which navigations can and cannot be vetoed.
enforceNavigationGuards
const enforceNavigationGuards: (store: Store, guards: ReadonlyArray<NavigationGuardAtom>) => (() => void)Makes navigation guard atoms actually block, for the given store. Call once near the root of an
app - or per component via jarl-react's useNavigationGuard - for every guard you want live.
Guards compose: the first that returns a message wins. Returns an unsubscribe function.
Nothing commits before a guard has had its say, so a blocked navigation leaves no history entry and the URL never flickers. What each navigation source gets:
- In-app navigation -
Link,useNavigate, a route atom orlocationAtomwritten directly - is vetoed atlocationAtom's write, and needs no browser support. - A same-document navigation from outside jarl - third-party
history.pushState, a fragment change, same-document back/forward - is vetoed through the Navigation API, and goes unguarded in a browser that lacks it. - Leaving the document - reload, a cross-document link, closing the tab - gets
beforeunload, so the browser shows its own wording instead of the guard's message.
Two things the platform refuses to let anyone veto, as anti-trapping measures: a cross-document
back/forward, and a back/forward repeated without interacting with the page in between, since
cancelling one consumes the activation that permits cancelling. A navigation the browser starts
for itself - the URL bar, a bookmark, the reload button - reaches beforeunload and nothing else.
Classes
Redirect
export class Redirect {
constructor(public readonly to: Path) {}
}A sentinel object meaning "actually, redirect to this instead".
Types
DefaultParams
export type DefaultParams = {};The param values a route binds. Empty for routes that bind none, such as a static segment.
NavOptions
export type NavOptions = { replace?: boolean };Extra argument when writing to a RouteAtom, e.g. set(routeAtom, values, { replace: true }).
replace navigates with history.replaceState rather than history.pushState.
ExtractRouteOptionalParam
export type ExtractRouteOptionalParam<PathType extends Path> = PathType extends `${infer Param}?`
? { readonly [k in Param]: string | undefined }
: PathType extends `${infer Param}*`
? { readonly [k in Param]: string | undefined }
: PathType extends `${infer Param}+`
? { readonly [k in Param]: string }
: { readonly [k in PathType]: string };The param name a single pattern segment binds, honouring its ?, * and + suffixes.
ExtractRouteParams
export type ExtractRouteParams<PathType extends string> = string extends PathType
? DefaultParams
: PathType extends `${infer _Start}:${infer ParamWithOptionalRegExp}/${infer Rest}`
? ParamWithOptionalRegExp extends `${infer Param}(${infer _RegExp})`
? ExtractRouteOptionalParam<Param> & ExtractRouteParams<Rest>
: ExtractRouteOptionalParam<ParamWithOptionalRegExp> & ExtractRouteParams<Rest>
: PathType extends `${infer _Start}:${infer ParamWithOptionalRegExp}`
? ParamWithOptionalRegExp extends `${infer Param}(${infer _RegExp})`
? ExtractRouteOptionalParam<Param>
: ExtractRouteOptionalParam<ParamWithOptionalRegExp>
: {};The full param object a :name-style path pattern binds, honouring each segment's ?/*/+
suffix via ExtractRouteOptionalParam.
RouteReturn
export type RouteReturn<T extends DefaultParams = DefaultParams> = {
reverse: (values: T) => string;
} & (
| {
match: true;
values: T;
exact: boolean;
rest: { path: string[] };
}
| {
match: false;
exact: false;
values: undefined;
}
);What reading any route atom gives you. match/exact say whether and how completely it
matches the current location, values holds the params it and its ancestors bound, rest
the path segments left for its children, and reverse turns param values back into a URL.
RouteAtom
export type RouteAtom<T extends DefaultParams> = WritableAtom<RouteReturn<T>, [T, NavOptions?], void>;A route: read it for its RouteReturn match state, write param values to it to navigate.
RouteValues
export type RouteValues<Route extends RouteAtom<any>> = Route extends RouteAtom<infer T> ? T : never;The param values a route atom binds: RouteValues<typeof postRoute> is that route's values.
RouteOptions
export type RouteOptions<Parent extends DefaultParams> = {
/** Route this one nests under, matching the segment after its parent's. Defaults to `rootRoute`. */
parent?: RouteAtom<Parent>;
};Options shared by every plain route-atom constructor (staticRouteAtom, paramRouteAtom, ...):
which route atom it composes under.
JarlLocation
export type JarlLocation = {
pathname?: string;
searchParams?: URLSearchParams;
hash?: string;
};The location every route atom reads: pathname, query params and hash.
RootRouteOptions
export type RootRouteOptions = {
/**
* Scopes the router to a subtree of the URL: the prefix is stripped from the pathname before
* matching begins, and prepended again by `reverse`/write. A location outside `basePath` makes
* the whole tree report `match: false`.
*/
basePath?: Path;
};Options for rootRouteAtom: how to scope the router to a subtree of the URL.
NumericRouteOptions
export type NumericRouteOptions<Parent extends DefaultParams> = RouteOptions<Parent> & {
/** Segments below this don't match. */
min?: number;
/** Segments above this don't match. */
max?: number;
};Options for numericRouteAtom: the common route options plus an inclusive numeric range.
EnumValues
export type EnumValues = readonly [string, ...string[]];The segment values an enumRouteAtom accepts: a non-empty tuple of string literals.
UnionRoutes
export type UnionRoutes = readonly [RouteAtom<any>, ...RouteAtom<any>[]];One or more route atoms, in the precedence order unionRouteAtom falls back through.
NotOptions
export type NotOptions = {
/**
* Whether only an exact (leaf) match counts as matching, rather than an ancestor match too.
* Defaults to `true`.
*/
exact?: boolean;
};Options for notAtom.
Path
export type Path = string;A URL path, optionally with a query string attached (/products/12?page=2).
QueryParamRouteOptions
export type QueryParamRouteOptions<Parent extends DefaultParams> = RouteOptions<Parent> & {
/** Makes a missing query param a non-match; by default it just yields `undefined`. */
required?: boolean;
};RouteOptions plus whether the param must be present for the route to match.
Store
export type Store = ReturnType<typeof createStore>;A jotai store, as returned by jotai's own createStore() - what every follow*/preloadRoutes-style
helper reads and writes against.
RouteLoader
export type RouteLoader<T extends DefaultParams, Data> = (
values: T,
get: Getter,
) => Promise<Data | Redirect | undefined>;Loads what a matched route needs. undefined means nothing exists at that address, so a route
gating on this loader doesn't match; a Redirect sends the app elsewhere instead, via
followAsyncRedirects.
AsyncRouteValues
export type AsyncRouteValues<T extends DefaultParams, Name extends string, Data> = T & {
readonly [key in Name]: Data;
};An async route's matched values: its parent's, plus the loaded object under Name.
AsyncRouteSnapshot
export type AsyncRouteSnapshot = {
pathname: string;
/** The loaded object, or `undefined` for "nothing exists here". */
data: unknown;
};One async route's settled load, carried from a server render into client hydration.
AsyncRouteAtom
export type AsyncRouteAtom<T extends DefaultParams, Name extends string, Data> = RouteAtom<
AsyncRouteValues<T, Name, Data>
> & {
/** The load on its own, with no bearing on matching and no lifecycle to wire up. */
readonly data: Atom<Promise<Data | Redirect | undefined>>;
/** Whether the load for the current location has yet to settle - true only while the parent route matches. */
readonly pending: Atom<boolean>;
/** The settled load the match reads, written by `preloadAsyncRoutes`, `hydrateAsyncRoutes` and `followAsyncRoutes`. */
readonly settled: WritableAtom<AsyncRouteSnapshot | null, [AsyncRouteSnapshot], void>;
};What asyncRouteAtom returns: a route atom, plus the seams its loader is driven through.
NavigationGuardAtom
export type NavigationGuardAtom = Atom<string | null>;A guard's verdict: the message to confirm a navigation with, or null to let it through.