Cookbook Router
Practical Patterns

Lifecycle instrumentation

Track route transitions, timings, and navigation errors with core lifecycle hooks.

Lifecycle hooks observe transition order around resolved route changes. They are useful for analytics, timings, logging, cleanup, and transition-scoped diagnostics.

Use middleware for redirects and rewrites. Use lifecycle when the transition itself should be observed or cancelled.

Add global lifecycle hooks

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

const routes = defineRoutes([
  {
    id: 'home',
    path: '/',
    view: 'home',
  },
  {
    id: 'reports',
    path: '/reports',
    view: 'reports',
  },
] as const);

let transitionStartedAt = 0;

const router = createRouter({
  routes,
  lifecycle: {
    beforeNavigate({ to }) {
      transitionStartedAt = performance.now();

      analytics.track('route_start', {
        to: to?.id,
      });
    },
    afterNavigate({ from, to }) {
      analytics.track('route_complete', {
        from: from?.id,
        to: to?.id,
        durationMs: performance.now() - transitionStartedAt,
      });
    },
    onNavigationError(error, { to }) {
      analytics.track('route_error', {
        to: to?.id,
        message:
          error instanceof Error
            ? error.message
            : String(error),
      });
    },
  },
});

Global lifecycle sees transitions across the route tree.

Add route lifecycle hooks

Route lifecycle belongs to the route definition.

const routes = defineRoutes([
  {
    id: 'reports',
    path: '/reports',
    view: 'reports',
    lifecycle: {
      beforeEnter() {
        reportsTimer.start();
      },
      afterEnter() {
        reportsTimer.finish();
      },
      beforeLeave() {
        reportsTimer.flush();
      },
      onError(error) {
        reportsTimer.fail(error);
      },
    },
  },
] as const);

Route lifecycle is tied to entering and leaving matched routes, not mounting and unmounting UI.

Cancel from lifecycle

beforeNavigate, beforeEnter, and beforeLeave can return false.

const routes = defineRoutes([
  {
    id: 'editor',
    path: '/editor',
    view: 'editor',
    lifecycle: {
      beforeLeave() {
        if (editor.hasUnsavedChanges()) {
          return false;
        }
      },
    },
  },
] as const);

Cancellation keeps the previous committed route state.

Where this bites

Lifecycle is not middleware

Lifecycle can cancel. It does not redirect or rewrite. Use middleware for destination changes.

Lifecycle is not component lifecycle

A renderer may keep views alive, remount them, stream them, or render them elsewhere. Router lifecycle follows route transitions.

Timings need failure paths

Transitions can fail, redirect, or be cancelled. Track completion and error paths explicitly instead of assuming every start reaches afterNavigate.

On this page