Cookbook Router
Practical Patterns

Links and navigation

Build route-ID hrefs, active checks, preloads, and programmatic navigation with the core router.

Core links are data plus event handling. The router builds hrefs, matches active state, preloads intent, and commits navigation. Your renderer decides whether those pieces become an anchor, button, command-palette row, or native-shell action.

Build from route identity

import {
  createRouter,
  defineRoutes,
} from '@cookbook/router';

const routes = defineRoutes([
  {
    id: 'users',
    path: '/users',
    children: [
      {
        id: 'users.details',
        path: '{userId:int}',
        view: 'users.details',
      },
    ],
  },
] as const);

const router = createRouter({ routes });

const target = {
  route: 'users.details',
  params: {
    userId: 42,
  },
  search: {
    tab: 'activity',
  },
  hash: 'summary',
} as const;

Build an href

router.href() returns a URL string and does not navigate.

const href = router.href(target);

href;
// '/users/42?tab=activity#summary'

Href generation validates required params and path constraints. Broken route links fail before navigation.

Your event layer owns browser defaults. Preserve modified clicks and other host-specific behavior before calling router navigation.

async function openUser(event: MouseEvent) {
  if (
    event.defaultPrevented ||
    event.button !== 0 ||
    event.metaKey ||
    event.altKey ||
    event.ctrlKey ||
    event.shiftKey
  ) {
    return;
  }

  event.preventDefault();

  await router.navigate.to(target);
}

For a command palette or native shell, there may be no DOM event at all:

await router.navigate.to(target);

Compute active state

Match the generated href against the router. Do not parse route IDs out of strings.

function isActive(targetHref: string) {
  return router.match(targetHref)?.id === router.state.match?.id;
}

Branch-active state is an application rule:

function isUnderUsers() {
  return router.state.match?.branch.some(
    (entry) => entry.id === 'users',
  ) ?? false;
}

Preload on intent

function preloadUser() {
  void router.preload(target);
}

function preloadHref(href: string) {
  void router.preloadHref(href);
}

Preloading warms route modules, lazy route views, and route-level preload hooks. It does not commit navigation.

interface LinkModel {
  readonly href: string;
  readonly active: boolean;
  readonly preload: () => void;
  readonly navigate: () => Promise<void>;
}

function createUserLink(userId: number): LinkModel {
  const target = {
    route: 'users.details',
    params: { userId },
  } as const;

  const href = router.href(target);

  return {
    href,
    active: router.match(href)?.id === router.state.match?.id,
    preload() {
      void router.preload(target);
    },
    async navigate() {
      await router.navigate.to(target);
    },
  };
}

Where this bites

Active state is not automatic

The core router exposes match state. It does not decide whether your UI wants exact active state, branch active state, prefix active state, or custom tab state.

Preload is not navigation

A successful preload does not change history, run a committed transition, or update router.state.location.

Preserve host behavior yourself

Core navigation helpers do not know whether a click should open a new tab, download a file, or let the host own the action. That belongs to the event layer.

On this page