Client router
The router is automatic. Grimoire intercepts same-origin link clicks, fetches the next route's data, and swaps the page without a full document load. You import from it only when you need to drive navigation yourself.
import { navigate } from "@sigil-dev/grimoire/client";navigate
await navigate("/books/12");The first argument is any path the router can resolve. The second controls history:
await navigate("/login", { history: "replace" }); // don't add an entry| Option | Effect |
|---|---|
"push" | Default. Adds a history entry, so Back returns here. |
"replace" | Swaps the current entry. Right after a redirect. |
"none" | Leaves history alone — the browser already moved (popstate). |
Overlapping navigations are safe: a newer one aborts the older silently, so a slow response can never commit over a fast one.
What gets intercepted
Not every click becomes a client navigation. A click is handled only when all of these hold:
- unmodified left click — no ctrl/meta/shift/alt, so new-tab and new-window still work
- an
<a>with notarget, ortarget="_self" - no
downloadattribute - no
rel="external" - a relative or same-origin
href— nothttp(s)://,//,#,mailto:,tel: - a path that matches a known page route
That last one is worth knowing: links to +server.ts routes navigate normally. The router lets the browser follow the response instead of doing a fetch-then-redirect dance, so an API endpoint or a file download behaves the way you expect.
Same-page anchors are also left alone, so a heading link just scrolls.
invalidate
After a mutation, the data behind the current route is stale. invalidate re-runs its load:
import { invalidate, invalidateAll } from "@sigil-dev/grimoire/client";
await invalidate("books"); // refetch if the route depends on "books"
await invalidateAll(); // refetch unconditionallyThe difference is that invalidate consults a dependency set. A route that never called depends() has no recorded keys, so it refetches conservatively — the safe default. A route that recorded keys refetches only when one of them matches, which keeps unrelated keys from causing work.
invalidate navigates to the current URL with history: "replace", so it does not fill the back button with entries the visitor never asked for.
depends
Mark which key a piece of data belongs to, so invalidate can be selective:
import { depends } from "@sigil-dev/grimoire/client";
export function load({ params }: LoadContext) {
depends(`book:${params.id}`);
return { book: getBook(Number(params.id)) };
}Then invalidating that same key refreshes exactly that page. Keys are plain strings; there is no registry to update.
Navigation lifecycle
Three callbacks, all from @sigil-dev/grimoire/client:
import {
beforeNavigate,
onNavigate,
afterNavigate,
} from "@sigil-dev/grimoire/client";| Callback | Runs | Use for |
|---|---|---|
beforeNavigate(cb) | Before the route changes | Confirming, cancelling |
onNavigate(cb) | As the route changes | Analytics, scroll reset |
afterNavigate(cb) | After the new page is in the DOM | Focus, measurement |
Each takes (url: URL) => void. beforeNavigate can also return false to cancel the navigation outright:
beforeNavigate((url) => {
if (formIsDirty && url.pathname !== "/") {
if (!confirm("Discard unsaved changes?")) return false;
}
});These run on client-side navigation only — a hard page load starts fresh, so a guard you need on every request belongs in hooks instead.
Next
- Directives and transitions — the
usearray - Form actions —
enhanceand the client router