Reference: Client router

Reference

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";
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
OptionEffect
"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 no target, or target="_self"
  • no download attribute
  • no rel="external"
  • a relative or same-origin href — not http(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 unconditionally

The 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.

Three callbacks, all from @sigil-dev/grimoire/client:

import {
	beforeNavigate,
	onNavigate,
	afterNavigate,
} from "@sigil-dev/grimoire/client";
CallbackRunsUse for
beforeNavigate(cb)Before the route changesConfirming, cancelling
onNavigate(cb)As the route changesAnalytics, scroll reset
afterNavigate(cb)After the new page is in the DOMFocus, 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

Edit this page