Cookbook Router
Practical Patterns

Custom history

Inject a custom RouterHistory for native shells, embedded apps, and controlled tests.

The router runtime consumes a RouterHistory. Browser, memory, and static histories are built in. Custom histories let another host own location storage and traversal.

Use a custom history for native shells, embedded apps, controlled tests, browser-like runtimes, and app hosts with their own navigation stack.

Implement the contract

import {
  createRouter,
  parseHref,
  type HistoryEvent,
  type RouterHistory,
  type RouterLocation,
} from '@cookbook/router';

function createHostHistory(
  initialHref = '/',
): RouterHistory {
  let location: RouterLocation = parseHref(initialHref);
  const listeners = new Set<
    (event: HistoryEvent) => void
  >();

  function emit(action: HistoryEvent['action']) {
    const event: HistoryEvent = {
      action,
      location,
    };

    for (const listener of listeners) {
      listener(event);
    }
  }

  return {
    mode: 'memory',

    get location() {
      return location;
    },

    push(href, state) {
      location = parseHref(href, { state });
      emit('push');
    },

    replace(href, state) {
      location = parseHref(href, {
        state,
        key: location.key,
      });
      emit('replace');
    },

    back() {
      hostNavigation.back();
    },

    forward() {
      hostNavigation.forward();
    },

    go(delta) {
      hostNavigation.go(delta);
    },

    listen(listener) {
      listeners.add(listener);

      return () => {
        listeners.delete(listener);
      };
    },
  };
}

parseHref() normalizes hrefs into RouterLocation shape. It does not match routes or parse typed URL state.

Inject it into the router

const router = createRouter({
  routes,
  history: createHostHistory('/'),
});

await router.start();

The router now reads and writes through the injected history.

Emit host changes

If the host changes location outside router navigation, update location and emit an event.

function applyHostLocation(nextHref: string) {
  location = parseHref(nextHref);

  emit('pop');
}

The router listener will resolve the new location.

Preserve replace keys

replace() should keep the current location key.

location = parseHref(href, {
  state,
  key: location.key,
});

Rendering integrations use keys for behaviors such as scroll restoration and transition identity.

External redirects

Implement redirectExternal only when the host can perform external navigation.

redirectExternal(href, mode) {
  if (mode === 'replace') {
    hostNavigation.replaceExternal(href);
    return;
  }

  hostNavigation.openExternal(href);
}

If redirectExternal is missing, external router redirects produce a router error state.

Where this bites

href must be origin-free

RouterLocation.href must equal pathname + search + hash. Do not store an origin in it.

Notify after visible changes

Listeners should be called after location has changed, not before.

Traversal is host-owned

back(), forward(), and go() may be no-ops if the host has no stack. That is valid, but it should be deliberate.

On this page