From 06944025258cae78ca527af4cace8ed7a2a8b635 Mon Sep 17 00:00:00 2001 From: Peter Hurst Date: Thu, 20 Aug 2026 03:33:31 +0100 Subject: [PATCH] =?UTF-8?q?refactor(atoms)!:=20789=20=E2=80=94=20name=20ev?= =?UTF-8?q?ery=20route=20atom=20`*RouteAtom`,=20and=20only=20route=20atoms?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The suffix now names the return type. An export whose value is a `RouteAtom` — one you can read for a `RouteReturn`, write param values to in order to navigate, or pass as another route's `parent` — ends in `RouteAtom`; any other atom ends in `Atom` alone. Five exports sat on the wrong side of that line, all of them routes. The pairs that read alike were the misleading ones: `rootAtom` and `locationAtom` are both ambient singletons but only one is a route, and `queryAtom` and `queryParamAtom` sound like two flavours of query state when only the second takes part in matching. The rule already held among the types — `RouteAtom` and `AsyncRouteAtom` against `NavigationGuardAtom` — so this brings the values into line with the types rather than inventing a convention, and records it in DESIGN-NOTES.md so later atoms land on the right side. Review feedback on the root pair inverted the initial landing: `rootRouteAtom`, the singleton instance, is not a factory, so per jotai core's own convention — a `...Atom`-suffixed export is a factory that returns an atom instance, not the instance itself — the bare instance drops the suffix (`rootRoute`) and the factory takes it (`createRootRouteAtom` becomes `rootRouteAtom`). This is the one place in the surface where a default instance and its own factory share a root word; every other `*RouteAtom` is a pure factory with no competing instance, so the return-type rule above still decides their names outright. DESIGN-NOTES.md records both the return-type rule and this narrower exception. Old names are not kept as deprecated aliases: two names per export would leave the surface less consistent than it started, which is the opposite of the point. BREAKING CHANGE: `rootAtom` is renamed `rootRoute`, `createRootAtom` to `rootRouteAtom`, `redirectAtom` to `redirectRouteAtom`, `queryParamAtom` to `queryParamRouteAtom`, and `validateAtom` to `validateRouteAtom`. Their options types follow: `RootOptions` becomes `RootRouteOptions` and `QueryParamOptions` becomes `QueryParamRouteOptions`. `locationAtom`, `queryAtom`, `notAtom`, `navigationGuardAtom` and every name already ending in `RouteAtom` are unchanged. Ticket: 789 --- CLAUDE.md | 3 ++ README.md | 43 ++++++++------- e2e/fixture-app/src/App.tsx | 6 +-- e2e/fixture-app/src/pages/Shell.tsx | 6 +-- e2e/fixture-app/src/routes.ts | 10 ++-- .../docs/src/content/guides/DataLoading.md | 6 +-- .../docs/src/content/guides/GettingStarted.md | 6 +-- .../docs/src/content/guides/PathVariables.md | 8 +-- packages/docs/src/content/history.md | 2 +- packages/docs/src/demos/BlogRoutingApp.tsx | 6 +-- packages/docs/src/demos/DataGridApp.tsx | 10 ++-- packages/docs/src/pages/Changelog.tsx | 4 +- packages/docs/src/pages/DemosIndex.tsx | 4 +- packages/docs/src/router/routes.ts | 4 +- packages/jarl-atoms/DESIGN-NOTES.md | 54 +++++++++++++++++-- packages/jarl-atoms/README.md | 8 +-- .../src/__tests__/asyncRouteAtom.test.ts | 2 +- .../jarl-atoms/src/__tests__/notAtom.test.ts | 2 +- .../src/__tests__/queryAtom.test.ts | 18 +++---- ...Atom.test.ts => redirectRouteAtom.test.ts} | 34 ++++++------ .../src/__tests__/requireMatch.test.ts | 4 +- .../src/__tests__/routeAtom.test.ts | 22 ++++---- ...Atom.test.ts => validateRouteAtom.test.ts} | 8 +-- packages/jarl-atoms/src/asyncRouteAtom.ts | 2 +- packages/jarl-atoms/src/index.ts | 6 +-- .../jarl-atoms/src/navigationGuardAtom.ts | 2 +- packages/jarl-atoms/src/notAtom.ts | 2 +- packages/jarl-atoms/src/queryAtom.ts | 12 ++--- .../{redirectAtom.ts => redirectRouteAtom.ts} | 20 +++---- .../src/{rootAtom.ts => rootRouteAtom.ts} | 10 ++-- packages/jarl-atoms/src/routeAtom.ts | 4 +- packages/jarl-atoms/src/types.ts | 2 +- .../{validateAtom.ts => validateRouteAtom.ts} | 4 +- packages/jarl-react/src/__tests__/fixtures.ts | 2 +- .../jarl-react/src/__tests__/hooks.test.tsx | 4 +- 35 files changed, 197 insertions(+), 143 deletions(-) rename packages/jarl-atoms/src/__tests__/{redirectAtom.test.ts => redirectRouteAtom.test.ts} (76%) rename packages/jarl-atoms/src/__tests__/{validateAtom.test.ts => validateRouteAtom.test.ts} (91%) rename packages/jarl-atoms/src/{redirectAtom.ts => redirectRouteAtom.ts} (85%) rename packages/jarl-atoms/src/{rootAtom.ts => rootRouteAtom.ts} (90%) rename packages/jarl-atoms/src/{validateAtom.ts => validateRouteAtom.ts} (89%) diff --git a/CLAUDE.md b/CLAUDE.md index 4cc1a218..bf06c0b3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,6 +22,9 @@ JARL ("JARL: Atomic Routing Library") is a controlled-component router for React - `infra/` — AWS CDK app provisioning the hosting for jarl.randomdev.co.uk. Also a separate npm project, kept out of the workspaces so it is never published: `infra/README.md`. +A new atom's name says what it returns: `*RouteAtom` if its value is a `RouteAtom`, `*Atom` +otherwise. See `packages/jarl-atoms/DESIGN-NOTES.md`. + The two packages are deliberately separate import paths: `jarl-react` does **not** re-export `jarl-atoms`. Consumers get route atoms from `jarl-atoms` and the React bindings from `jarl-react`, so the framework boundary stays visible and `jarl-atoms` is usable on its own. diff --git a/README.md b/README.md index 47b288d8..60f0e5d1 100644 --- a/README.md +++ b/README.md @@ -16,30 +16,30 @@ wanted something that did just this job extremely well, but without getting in t 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](https://jotai.org/) under the hood: each route is its own atom, with a link to a -parent atom and so on up to the [`rootAtom`](/api/jarl-atoms#rootatom); each one matching a -piece of the URL (normally a path segment) and telling you both whether it *currently* matches, +parent atom and so on up to the [`rootRoute`](/api/jarl-atoms#rootroute); 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 `if`s is enough to decide what components to -render, and navigation can be performed by *calling the atom setter*. (Convenience components +render, and navigation can be performed by _calling the atom setter_. (Convenience components like [``](/api/jarl-react#route) and [``](/api/jarl-react#switch) and of course the ubiquitous [``](/api/jarl-react#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 +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... +- 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 @@ -54,9 +54,9 @@ Declare some route atoms: ```ts // routes.ts -import { rootAtom, staticRouteAtom, paramRouteAtom } from "jarl-atoms"; +import { rootRoute, staticRouteAtom, paramRouteAtom } from "jarl-atoms"; -export const homeRoute = rootAtom; +export const homeRoute = rootRoute; export const aboutRoute = staticRouteAtom("about"); export const productsRoute = staticRouteAtom("products"); // The `productId` segment is bound into `values` when this route matches: @@ -75,7 +75,7 @@ import App from "./App"; createRoot(document.getElementById("root")!).render( - + , ); ``` @@ -110,7 +110,9 @@ import { Link } from "jarl-react"; const MainMenu = () => (