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) => string

Normalizes 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) => Path

Appends/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) => Path

Joins 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>) => string

Inverse 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) => Redirect

Constructs 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 Redirect

Type 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>) => void

Seeds 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.

const navigationGuardAtom: (guard: (get: Getter) => string | null) => NavigationGuardAtom

A 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 or locationAtom written directly - is vetoed at locationAtom'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.

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.

export type NavigationGuardAtom = Atom<string | null>;

A guard's verdict: the message to confirm a navigation with, or null to let it through.