Skip to content

Repository files navigation

JARL: Atomic Routing Library

Routing for the atomic age

latest npm version downloads CI Discord

If you just want the docs: JARL demos and documentation

What is a router?

A web router, fundamentally, is very simple: a mapping between URL and state. I have always wanted something that did just this job extremely well, but without getting in the way of or dictating application structure, and without forcing route matching logic into the component tree itself, where it never seemed to belong. JARL builds that mapping out of composable atoms using jotai under the hood: each route is its own atom, with a link to a parent atom and so on up to the rootAtom; each one matching a piece of the URL (normally a path segment) and telling you both whether it currently matches, as well as how to build a URL to that route based on a given state. Routing decisions in your application then decompose to very simple logic based on the current states of these atoms; a simple switch statement or series of ifs is enough to decide what components to render, and navigation can be performed by calling the atom setter. (Convenience components like <Route> and <Switch> and of course the ubiquitous <Link> are of course provided in the React package, if you want to build more compositionally; they all just accept atoms for parameters instead of type-unsafe strings.)

Because each route atom is an independent, subscribable unit of jotai state, a component that reads one only re-renders when that atom's derived value actually changes - it turns out this is incredibly efficient.

Features

  • Map URLs directly to state (and back again) - the URL becomes the source of truth
  • Composable route atoms - build nested/dynamic routes out of small, independent pieces
  • Framework-agnostic core (jarl-atoms) with lightweight React bindings (jarl-react)
  • Full querystring matching support
  • Resolve promises during routing (via jotai's own async atoms) and redirect if required
  • SSR/SSG-safe: the resolved location atom is hydratable per-render on the server
  • And much more...

Concrete Example

Add to your project (jotai is a peer dependency of both packages - install it alongside so there's exactly one copy in your tree):

npm install jarl-atoms jarl-react jotai

Declare some route atoms:

// routes.ts
import { rootAtom, staticRouteAtom, paramRouteAtom } from "jarl-atoms";

export const homeRoute = rootAtom;
export const aboutRoute = staticRouteAtom("about");
export const productsRoute = staticRouteAtom("products");
// The `productId` segment is bound into `values` when this route matches:
export const productRoute = paramRouteAtom("productId", { parent: productsRoute });

Wrap your app in a jotai <Provider> (this is what makes the shared location atom live) and render based on which route atom currently matches, using <Route>:

// main.tsx
import { createRoot } from "react-dom/client";
import { Provider } from "jotai";
import App from "./App";

createRoot(document.getElementById("root")!).render(
  <Provider>
    <App />
  </Provider>
);
// App.tsx
import { Route } from "jarl-react";
import { homeRoute, aboutRoute, productRoute } from "./routes";

const App = () => (
  <>
    <Route on={homeRoute} exact>
      <HomePage />
    </Route>
    <Route on={aboutRoute} exact>
      <AboutPage />
    </Route>
    <Route on={productRoute} exact>
      {({ productId }) => <ProductPage productId={productId} />}
    </Route>
  </>
);

export default App;

Wait, we missed something! How do you actually link to a page? JARL has a Link component much like other router libraries, but its unique feature is that it links directly to a route atom plus param values, generating the URL by reversing that same atom:

import { Link } from "jarl-react";

const MainMenu = () => (
  <nav>
    <Link route={homeRoute} exact>Home</Link>
    <Link route={aboutRoute}>About</Link>
    <Link route={productRoute} to={{ productId: "123" }}>
      Our Best Product Ever!
    </Link>
    <SearchForm />
  </nav>
);

These links use each route atom's reverse() to stringify the correct URL, e.g. the product link becomes <a href="/products/123">.

A component that needs to navigate programmatically (rather than render a plain link) can use the useNavigate hook instead:

import { atom, useAtom } from "jotai";
import { useNavigate } from "jarl-react";
import { queryParamAtom } from "jarl-atoms";

// A single named query-string param is its own composable route atom too:
const searchQueryRoute = queryParamAtom("q");

// Controlled search input value also tracked in an atom
const searchTextAtom = atom("");

const SearchForm = () => {
  const [searchText, setSearchText] = useAtom(searchTextAtom);
  const navigate = useNavigate(searchQueryRoute);
  return (
    <form onSubmit={(e) => { e.preventDefault(); navigate({ q: searchText }); }}>
      <input
        type="text"
        value={searchText}
        onChange={(e) => setSearchText(e.target.value)}
        placeholder="Enter search term"
      />
      <button type="submit">Search</button>
    </form>
  );
};

export default SearchForm;

That's all the basics! Hopefully this gave a flavour of the power and simplicity of this routing system. See the docs site for query strings, redirects, and data loading (resolving promises as part of a route match, jarl-atoms' resolvedAtom) in more depth.

Documentation

Detailed documentation, and demos with annotated code samples, can be viewed at the following address:

JARL demos and documentation

Changelog

Tests & Demos

git clone https://github.com/randomdevpete/jarl
cd jarl
npm install
npm run build

To run unit tests:

npm test

To run the docs/demo site (packages/docs):

npm run dev

To run E2E tests (using Playwright):

npm run test:e2e:install   # once, to install the suite's deps and browsers
npm run test:e2e

To check the packages as actually published on npm — installed from the registry into a clean consumer project, with no workspace linking (see e2e/registry-smoke):

npm run test:smoke:install
npm run test:smoke

Releases & versioning

jarl-atoms and jarl-react are versioned and released together via semantic-release, driven by Conventional Commits on master. Major version bumps are suppressed by design (breaking changes produce a minor bump instead) while the v2 API is still settling. See docs/release-strategy.md for the full details.

Community

We have a dedicated Discord server with CI announcements in #build: https://discord.gg/6yGq39rJ63

Or, come and join the conversation at Reactiflux: https://discordapp.com/invite/KWHrBDe

Credits

Built on jotai atoms and jotai-location for the underlying, SSR-safe browser history binding.

Some ideas and inspiration from redux-first-router: https://github.com/faceyspacey/redux-first-router

And to some extent the Autoroute feature of Orchard CMS, which I was a contributor to many moons ago ;)

Copyright

©2017-2026 Randomdev Ltd

Distributed under MIT license. See LICENSE.md for full details.

About

Just Another Routing Library for React

Resources

Stars

8 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages