diff --git a/packages/docs/src/App.tsx b/packages/docs/src/App.tsx
index 4217f81..1ec3920 100644
--- a/packages/docs/src/App.tsx
+++ b/packages/docs/src/App.tsx
@@ -11,6 +11,7 @@ import {
historyRoute,
demosIndexRoute,
basicRoutingDemoRoute,
+ switchRoutingDemoRoute,
blogRoutingDemoRoute,
dataGridDemoRoute,
complexRoutingDemoRoute,
@@ -23,6 +24,7 @@ import Changelog from "./pages/Changelog";
import History from "./pages/History";
import DemosIndex from "./pages/DemosIndex";
import BasicRoutingDemo from "./pages/BasicRoutingDemo";
+import SwitchRoutingDemo from "./pages/SwitchRoutingDemo";
import BlogRoutingDemo from "./pages/BlogRoutingDemo";
import DataGridDemo from "./pages/DataGridDemo";
import ComplexRoutingDemo from "./pages/ComplexRoutingDemo";
@@ -63,6 +65,9 @@ export const App = () => (
+
+
+
diff --git a/packages/docs/src/demos/SwitchRoutingApp.tsx b/packages/docs/src/demos/SwitchRoutingApp.tsx
new file mode 100644
index 0000000..b084142
--- /dev/null
+++ b/packages/docs/src/demos/SwitchRoutingApp.tsx
@@ -0,0 +1,113 @@
+import { atom, useAtomValue, useSetAtom } from "jotai";
+import { createRootAtom, paramRouteAtom, transformRouteAtom } from "jarl-atoms";
+import {
+ SwitchRoutingPage,
+ isSwitchRoutingPage,
+ switchRoutingPageLabels,
+ switchRoutingPages,
+} from "./switchRoutingPages";
+
+// The page this demo is mounted on, so everything below it is a plain module-level atom.
+const switchRoutingRoot = createRootAtom({ basePath: "/demos/switch-routing" });
+
+// One segment out of a fixed set: a paramRouteAtom narrowed by a transformRouteAtom, which also
+// types the value as the union the switch below needs.
+const pageRoute = transformRouteAtom<{ page: string }, { page: SwitchRoutingPage }>(
+ paramRouteAtom("page", { parent: switchRoutingRoot }),
+ ({ page }) => (isSwitchRoutingPage(page) ? { page } : undefined),
+ ({ page }) => ({ page }),
+);
+
+const currentPageAtom = atom((get): SwitchRoutingPage | "not-found" => {
+ const page = get(pageRoute);
+ if (page.match) return page.values.page;
+ return get(switchRoutingRoot).exact ? "home" : "not-found";
+});
+
+const HomePage = () => (
+
+
Home
+
+ Nothing here is a <Route>. One route atom holds which page the URL names, and a plain{" "}
+ switch on that value picks the component to render.
+
+
+);
+
+const AboutPage = () => (
+
+
About
+
+ The buttons above navigated here by writing to that same atom — {`navigate({ page: "about" })`}{" "}
+ — rather than by following a <Link>. Writing it set the URL; reading it back is what
+ moved the switch.
+
+
+);
+
+const ContactPage = () => (
+
+
Contact
+
+ Back and forward need nothing extra: the atom derives its value from the location, so the browser's history
+ moves the switch exactly the way a button does.
+
+
+);
+
+const NotFoundPage = () => (
+
+
No such page
+
+ The page route matches only the segments named above, so any other URL under this demo leaves it unmatched and the
+ switch falls through to this case.
+
+
+);
+
+const pageView = (page: SwitchRoutingPage | "not-found") => {
+ switch (page) {
+ case "home":
+ return ;
+ case "about":
+ return ;
+ case "contact":
+ return ;
+ case "not-found":
+ return ;
+ }
+};
+
+const SwitchRoutingNav = ({
+ current,
+ onNavigate,
+}: {
+ current: SwitchRoutingPage | "not-found";
+ onNavigate: (page: SwitchRoutingPage) => void;
+}) => (
+
+);
+
+/**
+ * Self-contained demo of the least routing machinery that still routes: a `switch` on one route
+ * atom's value picks the page, and navigation writes to that atom directly.
+ */
+export const SwitchRoutingApp = () => {
+ const page = useAtomValue(currentPageAtom);
+ const navigate = useSetAtom(pageRoute);
+
+ return (
+ <>
+ navigate({ page: next })} />
+ {pageView(page)}
+ >
+ );
+};
+
+export default SwitchRoutingApp;
diff --git a/packages/docs/src/demos/switchRoutingPages.ts b/packages/docs/src/demos/switchRoutingPages.ts
new file mode 100644
index 0000000..9dfe029
--- /dev/null
+++ b/packages/docs/src/demos/switchRoutingPages.ts
@@ -0,0 +1,19 @@
+/** Every page this demo has: the switch's cases, and the only segments its page route matches. */
+export const switchRoutingPages = ["home", "about", "contact"] as const;
+
+export type SwitchRoutingPage = (typeof switchRoutingPages)[number];
+
+export const isSwitchRoutingPage = (segment: string): segment is SwitchRoutingPage =>
+ (switchRoutingPages as readonly string[]).includes(segment);
+
+export const switchRoutingPageLabels: Record = {
+ home: "Home",
+ about: "About",
+ contact: "Contact",
+};
+
+/** Every concrete path this demo's SSG build should prerender. */
+export const switchRoutingStaticPaths = (): string[] => [
+ "/demos/switch-routing",
+ ...switchRoutingPages.map((page) => `/demos/switch-routing/${page}`),
+];
diff --git a/packages/docs/src/pages/DemosIndex.tsx b/packages/docs/src/pages/DemosIndex.tsx
index 7d52440..1e0cecb 100644
--- a/packages/docs/src/pages/DemosIndex.tsx
+++ b/packages/docs/src/pages/DemosIndex.tsx
@@ -6,6 +6,7 @@ import {
blogRoutingDemoRoute,
complexRoutingDemoRoute,
dataGridDemoRoute,
+ switchRoutingDemoRoute,
} from "../router/routes";
export const DemosIndex = () => (
@@ -26,6 +27,13 @@ export const DemosIndex = () => (
— a nested router-within-a-router built from staticRouteAtom/paramRouteAtom and
the atoms-based Link/Route components.
+
+
+ Switch-statement routing
+ {" "}
+ — the same job with no routing components at all: a switch on one route atom's value
+ picks the page, and the nav buttons navigate by writing to that atom instead of rendering a Link.
+
Blog routing
diff --git a/packages/docs/src/pages/SwitchRoutingDemo.tsx b/packages/docs/src/pages/SwitchRoutingDemo.tsx
new file mode 100644
index 0000000..4a1022d
--- /dev/null
+++ b/packages/docs/src/pages/SwitchRoutingDemo.tsx
@@ -0,0 +1,15 @@
+import { SwitchRoutingApp } from "../demos/SwitchRoutingApp";
+import DemoPage from "../lib/DemoPage";
+import demoSource from "../demos/SwitchRoutingApp.tsx?raw";
+
+export const SwitchRoutingDemo = () => (
+
+
+
+);
+
+export default SwitchRoutingDemo;
diff --git a/packages/docs/src/router/routes.ts b/packages/docs/src/router/routes.ts
index 8b0c945..d8d1df4 100644
--- a/packages/docs/src/router/routes.ts
+++ b/packages/docs/src/router/routes.ts
@@ -9,6 +9,7 @@ import { atom } from "jotai";
import { asyncRouteAtom, notAtom, rootAtom, staticRouteAtom, paramRouteAtom } from "jarl-atoms";
import { blogStaticPaths } from "../demos/blogPosts";
import { complexRoutingStaticPaths } from "../demos/complexRoutingSamples";
+import { switchRoutingStaticPaths } from "../demos/switchRoutingPages";
import { articleSlugs, findArticle } from "../demos/asyncArticles";
import { changelogStaticPaths } from "../pages/changelogEntries";
@@ -32,6 +33,10 @@ export const demosIndexRoute = staticRouteAtom("demos");
export const basicRoutingDemoRoute = staticRouteAtom("basic-routing", { parent: demosIndexRoute });
export const basicRoutingDemoPageRoute = paramRouteAtom("page", { parent: basicRoutingDemoRoute });
+// Switch-statement routing demo: the site's own mount point. The demo reads one page route atom
+// and switches on its value, on its own basePath-scoped root inside SwitchRoutingApp.
+export const switchRoutingDemoRoute = staticRouteAtom("switch-routing", { parent: demosIndexRoute });
+
// Blog routing demo: the site's own mount point. The demo's own /:year/:month/:day/:slug tree
// lives inside BlogRoutingApp, on its own basePath-scoped root.
export const blogRoutingDemoRoute = staticRouteAtom("blog-routing", { parent: demosIndexRoute });
@@ -67,6 +72,7 @@ const exactRouteMissedAtom = notAtom(
demosIndexRoute,
basicRoutingDemoRoute,
basicRoutingDemoPageRoute,
+ switchRoutingDemoRoute,
blogRoutingDemoRoute,
dataGridDemoRoute,
complexRoutingDemoRoute,
@@ -76,9 +82,9 @@ const exactRouteMissedAtom = notAtom(
/**
* Whether the current location has nothing behind it, which is what makes a server render's
- * *status code* right and not just its HTML. Everything under the changelog's, the blog demo's
- * and the complex-routing demo's mounts counts as found - all three route their own subtree and
- * render their own not-found views. The async demo gets no such blanket, and lists
+ * *status code* right and not just its HTML. Everything under the changelog's, the switch demo's,
+ * the blog demo's and the complex-routing demo's mounts counts as found - all four route their own
+ * subtree and render their own not-found views. The async demo gets no such blanket, and lists
* `asyncArticleRoute` rather than `asyncLookupSlugRoute`: an unknown slug is a genuine miss, even
* though the demo page still renders its own not-found view.
*/
@@ -86,6 +92,7 @@ export const notFoundAtom = atom(
(get) =>
get(exactRouteMissedAtom) &&
!get(changelogRoute).match &&
+ !get(switchRoutingDemoRoute).match &&
!get(blogRoutingDemoRoute).match &&
!get(complexRoutingDemoRoute).match,
);
@@ -117,6 +124,7 @@ export const staticPaths: string[] = [
"/demos",
"/demos/basic-routing",
"/demos/basic-routing/about",
+ ...switchRoutingStaticPaths(),
...blogStaticPaths(),
"/demos/data-grid",
...complexRoutingStaticPaths(),