Askr
Guides

Build an SPA

Use build an spa with the current published Askr packages.

  • @askrjs/askr0.0.59

How to use build an spa

Create one route registry, mount it with createSPA, and keep browser-only services behind lifecycle-owned components.

  1. Import the published @askrjs/askr entrypoint.
  2. Keep build an spa configuration next to the component, route, or server composition root that owns it.
  3. Handle pending, unavailable, cancellation, and failure states, then verify the production path.
import { createSPA } from '@askrjs/askr/boot';
import { createRouteRegistry, route } from '@askrjs/askr/router';

const registry = createRouteRegistry(() => {
  route('/', DashboardPage);
  route('/projects/:projectId', ProjectPage);
});

await createSPA({ root: document.getElementById('app')!, registry });

Goal and architecture

A client-only SPA mounts once, owns routing in the browser, and never touches the server beyond static asset delivery and whatever APIs it calls at runtime. You register routes with `registerRoutes()` or build a registry via `createRouteRegistry(() => { ... })`, then hand that to `createSPA({ root: '#app', registry })`. `@askrjs/vite`'s `askr()` plugin is the only build-time piece you need — there's no server plugin, no `askrServer()` entry, and no document markers to manage.

Implementation

Define routes with `route(path, Component)` or the nested `page()`/`group()`/`index()` helpers inside your registration function, and read the matched params straight from the component's first argument — Askr infers `RoutePathParams<Path>` from the literal path string, so `route('/users/{id}', UserPage)` gives `UserPage` a typed `id: string` without you writing the type by hand. State inside components is `state()`/`derive()`: read through the getter, write through the setter, no proxies. Wire up `<Link>` or `navigate()` for transitions and `Outlet()` in any layout component that needs to render nested routes.

Failure states

Wrap render-time failures in `ErrorBoundary` with a `fallback` — it logs the underlying error and can reset when your app state changes, which matters in an SPA where a bad route param or a malformed API response shouldn't take down the whole shell. Register a `fallback(Component)` route for unmatched paths so users don't hit a blank screen on a typo'd URL. If a route has an `auth` policy and the resolver returns `deny`/`redirect`, make sure you've actually mounted with `auth` options on `createSPA`, or those policies never run.

Verification

Click through every route you registered and confirm the URL bar, browser back/forward, and scroll position all behave — `scrollRestoration` on `SPAConfig` is opt-in, so verify it's doing what you expect rather than assuming a default. Force a render-time error in a leaf component and confirm the nearest `ErrorBoundary` catches it instead of a blank page. Finally check `getManifest()` against your actual route tree; a stale manifest is the most common source of "routes work in dev but not in the built app."