JARL
JARL: Atomic Routing Library.
The production grade "batteries included" controlled component router.
If you just want the docs: JARL demos and documentation
Why another router?
A web router simply performs a mapping between URL and state. I wanted something that did this
job extremely well without getting in the way of application structure and without mixing
routing logic up with the component tree. JARL builds that mapping out of composable
jotai atoms: each route is its own atom, matching a piece of the URL and
telling you both whether it currently matches and how to build a URL back out of param values.
Routing decisions in your application are then just React state reads via hooks. (There's no
<Switch/> component either - a <Route> per page, or a plain conditional, does the job.)
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 - not on every navigation everywhere in the tree, which is where the "atomic" in "Atomic Routing Library" comes from.
Features
- Composable route atoms - build nested/dynamic routes out of small, independent pieces
- Framework-agnostic core (
jarl-atoms) with thin, hooks-first 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 shared location atom is seedable 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} to={{}} exact>Home</Link>
<Link route={aboutRoute} to={{}}>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 { useState } from "react";
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");
const SearchForm = () => {
const [searchText, setSearchText] = useState("");
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:
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-2018 Downplay Ltd
Distributed under MIT license. See LICENSE for full details.