createRouter()
createRouter(config) builds a router around a single current signal. Subscribing to that signal is how every other piece of router-aware UI (RouterView, Link, components that read params / query) stays reactive.
Ships from the separate package @whisq/router, versioned lockstep with @whisq/core:
npm install @whisq/routerSignature
Section titled “Signature”import { createRouter } from "@whisq/router";import type { Router, RouterConfig, RouteConfig, RouteState, NavigationGuard, AfterGuard, ScrollBehaviorOption,} from "@whisq/router";
function createRouter(config: RouterConfig): Router;Quick start
Section titled “Quick start”import { createRouter, RouterView } from "@whisq/router";import { mount } from "@whisq/core";import { Home } from "./routes/Home";import { UserDetail } from "./routes/UserDetail";import { NotFound } from "./routes/NotFound";
const router = createRouter({ routes: [ { path: "/", component: Home }, { path: "/users/:id", component: UserDetail }, { path: "*", component: NotFound }, ],});
mount(RouterView(router), document.getElementById("app")!);RouterConfig
Section titled “RouterConfig”| Field | Type | Description |
|---|---|---|
routes | RouteConfig[] | Route table. First match wins; order matters. |
beforeEach | NavigationGuard (optional) | Runs before every navigation. Return false to cancel, a string to redirect, or void / true to proceed. |
afterEach | AfterGuard (optional) | Runs after every successful navigation. Side-effect only — return value is ignored. |
scrollBehavior | ScrollBehaviorOption (optional, default "top") | "top" scrolls to 0,0; "restore" remembers and restores per-path scroll on back/forward; "auto" and false leave scroll untouched. |
RouteConfig
Section titled “RouteConfig”| Field | Type | Description |
|---|---|---|
path | string | Pattern: static (/about), param (/users/:id), or wildcard (*). :name extracts into params.name. |
component | RouteComponent | Either a sync (params) => WhisqNode or a lazy () => import("./X") whose default export is the component. |
children | RouteConfig[] (optional) | Nested routes. When a route has children, the parent matches as a prefix; children render into a depth-1 RouterView. |
meta | Record<string, unknown> (optional) | Arbitrary per-route metadata. Merged into the resolved RouteState.meta in matched order. |
beforeEnter | NavigationGuard (optional) | Per-route guard. Runs after the global beforeEach for every matched route in the chain. |
Lazy components work by returning a promise:
{ path: "/admin", component: () => import("./routes/Admin") }// ./routes/Admin.ts must `export default` the component.Guards — NavigationGuard and AfterGuard
Section titled “Guards — NavigationGuard and AfterGuard”type NavigationGuard = ( to: RouteState, from: RouteState | null,) => boolean | string | void;
type AfterGuard = (to: RouteState, from: RouteState | null) => void;Return-value semantics for NavigationGuard:
| Returned value | Effect |
|---|---|
true / undefined | Proceed |
false | Cancel navigation; URL is reverted on push-navigations (back/forward keep the new URL) |
string | Redirect to that path; the redirect itself is pushed as a replacement of the current history entry |
Guards run in order: global beforeEach first, then each matched route’s beforeEnter top-down. The first false or string short-circuits the chain.
createRouter({ routes: [...], beforeEach: (to) => { if (to.meta.requiresAuth && !isLoggedIn()) return "/login"; },});Router interface
Section titled “Router interface”interface Router { current: ReadonlySignal<RouteState>; navigate(path: string): void; back(): void; forward(): void; dispose(): void;}| Member | Description |
|---|---|
current | The reactive RouteState signal. Read as router.current.value (or pass () => router.current.value to computed / effects). |
navigate(path) | Push-state navigation. Runs guards. Equivalent to what Link dispatches on click. |
back() | Calls window.history.back(). popstate re-runs guards. |
forward() | Calls window.history.forward(). |
dispose() | Detaches the internal popstate listener. Call when hot-reloading or tearing down tests — without this the listener leaks across module re-loads. |
current is a ReadonlySignal<RouteState> — you cannot write to it directly. Navigation is the only way to change the route.
ScrollBehaviorOption
Section titled “ScrollBehaviorOption”| Value | Behaviour |
|---|---|
"top" (default) | Scroll to (0, 0) on every navigation |
"restore" | Save scroll on leave; restore on back/forward. Forward navigations still scroll to top |
"auto" | Leave scroll untouched (browser default on popstate — useful with native anchor scrolling) |
false | Same as "auto" — never touches window.scrollTo |
Scroll actions (both the "top" scrollTo and "restore"’s saved-position restore) are deferred to requestAnimationFrame so the new route has mounted before the scroll lands.
Semantics
Section titled “Semantics”- Single signal per router.
router.currentis the only reactive input — everything downstream (RouterView,Link’s active class, components readingparams) derives from it. - Guards are synchronous. There’s no async-guard ceremony — if you need async (e.g. auth check), gate the redirect on a cached signal instead of awaiting inside the guard.
- Wildcards are terminal. A
{ path: "*" }route matches everything and should come last in the table. - Per-route params merge up. Nested routes see their own params plus all ancestors’ params in
RouteState.params, in matched order.
See also
Section titled “See also”/api/routerview/— rendering the matched component (and nested routes)./api/link/— push-state navigation anchors./api/route-state/— reading the current route reactively./guides/routing/— routing patterns and end-to-end examples./examples/router-basics/— small multi-page example with params, nested routes, and a guard.
Docs current to v0.1.0-alpha.9 . All releases →