Skip to content
Whisq v0.1.0-alpha.9

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:

Terminal window
npm install @whisq/router
import { createRouter } from "@whisq/router";
import type {
Router, RouterConfig, RouteConfig, RouteState,
NavigationGuard, AfterGuard, ScrollBehaviorOption,
} from "@whisq/router";
function createRouter(config: RouterConfig): Router;
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")!);
FieldTypeDescription
routesRouteConfig[]Route table. First match wins; order matters.
beforeEachNavigationGuard (optional)Runs before every navigation. Return false to cancel, a string to redirect, or void / true to proceed.
afterEachAfterGuard (optional)Runs after every successful navigation. Side-effect only — return value is ignored.
scrollBehaviorScrollBehaviorOption (optional, default "top")"top" scrolls to 0,0; "restore" remembers and restores per-path scroll on back/forward; "auto" and false leave scroll untouched.
FieldTypeDescription
pathstringPattern: static (/about), param (/users/:id), or wildcard (*). :name extracts into params.name.
componentRouteComponentEither a sync (params) => WhisqNode or a lazy () => import("./X") whose default export is the component.
childrenRouteConfig[] (optional)Nested routes. When a route has children, the parent matches as a prefix; children render into a depth-1 RouterView.
metaRecord<string, unknown> (optional)Arbitrary per-route metadata. Merged into the resolved RouteState.meta in matched order.
beforeEnterNavigationGuard (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.
type NavigationGuard = (
to: RouteState,
from: RouteState | null,
) => boolean | string | void;
type AfterGuard = (to: RouteState, from: RouteState | null) => void;

Return-value semantics for NavigationGuard:

Returned valueEffect
true / undefinedProceed
falseCancel navigation; URL is reverted on push-navigations (back/forward keep the new URL)
stringRedirect 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";
},
});
interface Router {
current: ReadonlySignal<RouteState>;
navigate(path: string): void;
back(): void;
forward(): void;
dispose(): void;
}
MemberDescription
currentThe 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.

ValueBehaviour
"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)
falseSame 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.

  • Single signal per router. router.current is the only reactive input — everything downstream (RouterView, Link’s active class, components reading params) 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.

Docs current to v0.1.0-alpha.9 . All releases →