Path Variables

Now we've seen how to set up some static routes, let's look at something a bit more advanced. Routing isn't much use if we have to define every single URL statically!

paramRouteAtom binds a single dynamic path segment to a named value, composed on top of a parent route atom (any route atom - rootRoute, a staticRouteAtom, or another paramRouteAtom, for nested dynamic segments):

routes.ts:

import { staticRouteAtom, paramRouteAtom } from "jarl-atoms";

export const productsRoute = staticRouteAtom("products");
export const productRoute = paramRouteAtom("productId", { parent: productsRoute });

This matches /products/:productId-shaped URLs: productsRoute consumes the products segment, and productRoute binds whatever segment follows it to productId. Reading productRoute (via useRoute, or the values passed to <Route>'s render-prop children) gives you { productId: "123" } once it matches:

import { useEffect, useState } from "react";
import { Route } from "jarl-react";
import { productRoute } from "./routes";

const ProductPage = ({ productId }: { productId: string }) => {
  const [product, setProduct] = useState<Product | null>(null);
  useEffect(() => {
    fetch(`/api/products/${productId}`)
      .then((result) => result.json())
      .then(setProduct);
  }, [productId]);
  return product ? <ProductView product={product} /> : "Loading...";
};

export default () => (
  <Route on={productRoute} exact>
    {({ productId }) => <ProductPage productId={productId} />}
  </Route>
);

In this example we're optimistically loading the page and displaying a "Loading..." spinner while the data loads. See the Data Loading guide for a better way to manage this, by moving the fetch into the route atom itself.

Linking to a dynamic route works the same way as a static one - just pass the param values as to:

<Link route={productRoute} to={{ productId: "123" }}>
  Our Best Product Ever!
</Link>

Segments from a fixed set

A dynamic segment usually isn't dynamic in the sense of "anything at all" - it's one of a handful of known values. enumRouteAtom matches exactly those and nothing else, and types the value it binds as the union of them rather than as string:

import { staticRouteAtom, paramRouteAtom, enumRouteAtom } from "jarl-atoms";

const productTabs = ["overview", "reviews", "specs"] as const;

export const productsRoute = staticRouteAtom("products");
export const productRoute = paramRouteAtom("productId", { parent: productsRoute });
export const productTabRoute = enumRouteAtom("tab", productTabs, { parent: productRoute });

/products/123/reviews matches with { productId: "123", tab: "reviews" }, where tab is "overview" | "reviews" | "specs" - so a switch over it is exhaustive with no default case to write, and <Link route={productTabRoute} to={{ productId: "123", tab: "reveiws" }}> is a typo the compiler catches rather than a dead link. /products/123/pricing doesn't match at all, so it falls through to whatever the app renders for an unknown URL instead of reaching the tab page with a value it has nothing to show for.

numericRouteAtom does the same job for a segment that has to be a whole number, optionally within a min/max range, binding it as a number rather than a string. For a constraint neither one covers - a checksum, a slug in a database, a rule spanning several segments - see transformRouteAtom and validateRouteAtom in the API reference.

Query parameters

Dynamic path segments aren't the only way to carry a value in a URL - queryParamRouteAtom (from jarl-atoms) does the same job for a single named query-string parameter, composed on top of a parent route atom exactly like paramRouteAtom, except it doesn't consume a path segment:

import { staticRouteAtom, queryParamRouteAtom } from "jarl-atoms";

export const searchRoute = staticRouteAtom("search");
export const searchQueryRoute = queryParamRouteAtom("q", { parent: searchRoute });

searchQueryRoute matches whenever /search does, with values.q set to the current ?q= value (or undefined if it's missing - pass { required: true } to make a missing query param a non-match instead, if the page needs one to render anything meaningful). Reading, writing, and linking all work exactly as they do for a path variable - reverse/Link append or update just that one query param, leaving the path and any other query params untouched.

Need the whole query string as an object, rather than one named param at a time? queryAtom (also from jarl-atoms) reads/writes it all at once.