Whisq v0.1.0-alpha.9
Router basics
A minimal routed app that exercises the parts of @whisq/router you reach for on day one: dynamic params, nested routes, a beforeEach guard for auth-gating, and reactive active-link styling via router.current.
What You’ll Build
Section titled “What You’ll Build”- Public routes:
/(home),/users,/users/:id,/users/:id/edit - A protected
/adminroute gated by abeforeEachguard - Nested routes under
/users— a shared layout with its own sub-nav - A
navwith active-link styling that highlights the current top-level route - A wildcard
*fallback
Install
Section titled “Install”npm install @whisq/core @whisq/routerThe router
Section titled “The router”import { createRouter } from "@whisq/router";import { Home } from "./routes/Home";import { UsersLayout } from "./routes/UsersLayout";import { UsersList } from "./routes/UsersList";import { UserDetail } from "./routes/UserDetail";import { UserEdit } from "./routes/UserEdit";import { Admin } from "./routes/Admin";import { Login } from "./routes/Login";import { NotFound } from "./routes/NotFound";import { isLoggedIn } from "./stores/session";
export const router = createRouter({ routes: [ { path: "/", component: Home }, { path: "/login", component: Login },
{ path: "/users", component: UsersLayout, children: [ { path: "", component: UsersList }, // /users { path: ":id", component: UserDetail }, // /users/:id { path: ":id/edit", component: UserEdit }, // /users/:id/edit ], },
{ path: "/admin", component: Admin, meta: { requiresAuth: true }, },
{ path: "*", component: NotFound }, ],
beforeEach: (to) => { // Guard: gate any route with meta.requiresAuth. // Return a string to redirect; return false to cancel; return void/true to proceed. if (to.meta.requiresAuth && !isLoggedIn()) { return "/login"; } },
scrollBehavior: "restore",});The app shell
Section titled “The app shell”import { component, div, header, nav, main } from "@whisq/core";import { Link, RouterView } from "@whisq/router";import { router } from "./router";
const isActive = (prefix: string) => () => router.current.value.path.startsWith(prefix) ? "active" : "";
export const App = component(() => div( header( nav( Link({ href: "/", router, class: () => router.current.value.path === "/" ? "active" : "" }, "Home"), Link({ href: "/users", router, class: isActive("/users") }, "Users"), Link({ href: "/admin", router, class: isActive("/admin") }, "Admin"), ), ), main(RouterView(router)), ),);import { mount } from "@whisq/core";import { App } from "./App";
mount(App({}), document.getElementById("app")!);The nested /users layout
Section titled “The nested /users layout”import { component, div, h2, nav } from "@whisq/core";import { Link, RouterView } from "@whisq/router";import { router } from "../router";
export const UsersLayout = component(() => div( h2("Users"), nav( Link({ href: "/users", router }, "All"), Link({ href: "/users/1", router }, "Alice"), Link({ href: "/users/1/edit", router }, "Edit Alice"), ), // depth 1 — renders UsersList / UserDetail / UserEdit based on child match. RouterView(router, 1), ),);Each child route gets its own component; RouterView(router, 1) swaps between them as the child path changes.
A dynamic-param route
Section titled “A dynamic-param route”// Snapshot pattern: `id` comes from the render callback's params argument.// RouterView re-mounts this component on every route change, so `id` is// always fresh for the current match — no reactive wrapper needed.import { h1, p, div } from "@whisq/core";import { router } from "../router";
export const UserDetail = ({ id }: Record<string, string>) => div( h1(`User ${id}`), // router.current.value.path IS reactive across component re-mounts — // useful when the same component instance survives multiple matches. p(() => `Path: ${router.current.value.path}`), );// Reactive pattern: read the current id from router.current inside a getter.// Equivalent to the snapshot form above when RouterView re-mounts per match,// but explicit about the reactive source if you're extending this later.import { h1, p, div } from "@whisq/core";import { router } from "../router";
export const UserEdit = () => div( h1(() => `Editing user ${router.current.value.params.id}`), p("(imagine a form here)"), );A session store + guard target
Section titled “A session store + guard target”import { signal } from "@whisq/core";
const loggedIn = signal(false);
export const isLoggedIn = () => loggedIn.peek();export const login = () => { loggedIn.value = true; };export const logout = () => { loggedIn.value = false; };The beforeEach guard calls isLoggedIn() at navigation time — .peek() is used so the guard doesn’t subscribe the navigation to the signal. The login route flips the signal, then programmatically navigates:
import { component, div, h1, p, button } from "@whisq/core";import { router } from "../router";import { login } from "../stores/session";
export const Login = component(() => div( h1("Login"), p("Pretend this is a form."), button({ onclick: () => { login(); router.navigate("/admin"); }, }, "Log in and go to /admin"), ),);Admin, Home, UsersList, NotFound
Section titled “Admin, Home, UsersList, NotFound”import { div, h1 } from "@whisq/core";export const Home = () => div(h1("Home"));
// src/routes/Admin.tsimport { component, div, h1, button } from "@whisq/core";import { router } from "../router";import { logout } from "../stores/session";export const Admin = component(() => div( h1("Admin"), button({ onclick: () => { logout(); router.navigate("/"); } }, "Log out"), ),);
// src/routes/UsersList.tsimport { div, ul, li } from "@whisq/core";import { Link } from "@whisq/router";import { router } from "../router";export const UsersList = () => div( ul( li(Link({ href: "/users/1", router }, "Alice")), li(Link({ href: "/users/2", router }, "Bob")), ), );
// src/routes/NotFound.tsimport { div, h1, p } from "@whisq/core";export const NotFound = () => div(h1("Not found"), p(() => "404"));Key concepts
Section titled “Key concepts”- Single
router.currentsignal. Every reactive router read —RouterView’s mounted component,Link’s active class, any component readingparams/query— ultimately subscribes to this one signal. - Nested
RouterView(router, depth). Matching nests under children;RouterViewatdepth = 1renders the matched child. Further nesting can godepth = 2, etc. - Guard return values.
beforeEachreturning a string redirects; returningfalsecancels; returningvoid(ortrue) proceeds. The guard is synchronous — for async auth checks, read a cached signal (via.peek()) instead of awaiting. metais how guards find routes. Settingmeta: { requiresAuth: true }lets a singlebeforeEachhandle every protected route without enumerating paths.- Active links. No dedicated
activeprop — use a reactiveclassgetter readingrouter.current.value.path. Exact match with===, prefix match with.startsWith().
Next Steps
Section titled “Next Steps”- Routing guide — patterns and walkthroughs.
/api/createrouter/— config reference./api/routerview/— the mounting component./api/link/— navigation anchors./api/route-state/— reactive reads.
Docs current to v0.1.0-alpha.9 . All releases →