Cookbook Router
Practical Patterns

Layouts

Render persistent layout shells with context.outlet and layout views.

Layouts are route-owned shells. They wrap the rendered child branch and can expose named slots.

In the core renderer, the equivalent of an outlet is context.outlet.

Declare a layout view

import {
  defineRoutes,
  renderRouteMatch,
  type RouteMatch,
} from '@cookbook/router';

const routes = defineRoutes([
  {
    id: 'dashboard',
    path: '/dashboard',
    layout: {
      view: 'dashboard.layout',
    },
    children: [
      {
        id: 'dashboard.home',
        index: true,
        view: 'dashboard.home',
      },
      {
        id: 'dashboard.reports',
        path: 'reports',
        view: 'dashboard.reports',
      },
    ],
  },
] as const);

The layout view belongs to dashboard. It wraps whichever child route is active.

Render the layout

interface Node {
  readonly kind: string;
  readonly routeId?: string;
  readonly children?: readonly Node[];
}

function renderDashboard(
  match: RouteMatch | null,
): Node | null {
  return renderRouteMatch<string, Node | null>(match, {
    fallback: null,
    renderView(view, context) {
      return {
        kind: view,
        routeId: context.match.id,
      };
    },
    renderLayout(view, context) {
      return {
        kind: view,
        routeId: context.ownerRouteId,
        children: [
          { kind: 'dashboard.nav' },
          context.outlet,
        ].filter((child): child is Node => child !== null),
      };
    },
    renderEmpty() {
      return null;
    },
  });
}

context.outlet is already-rendered child output. The layout decides where to place it.

Layouts can own fallback policy

Layout-level loading and error views are inherited by descendants rendered inside that layout.

const routes = defineRoutes([
  {
    id: 'dashboard',
    path: '/dashboard',
    layout: {
      view: 'dashboard.layout',
      loading: 'dashboard.loading',
      error: 'dashboard.error',
    },
    children: [
      {
        id: 'dashboard.reports',
        path: 'reports',
        view: 'dashboard.reports',
      },
    ],
  },
] as const);

The renderer still decides how to install the boundary.

Pathless layouts

A pathless layout can group UI without adding a URL segment.

const routes = defineRoutes([
  {
    id: 'app',
    layout: {
      view: 'app.layout',
    },
    children: [
      {
        id: 'home',
        path: '/',
        view: 'home.page',
      },
    ],
  },
] as const);

A pathless route can participate in the branch but cannot produce a standalone href.

Where this bites

A layout is not the same as a route view

A route view renders at that route position. A layout view wraps the route output after the route and descendants have been rendered.

context.outlet is already rendered

Do not try to match child routes inside a layout callback. The traversal already did that work.

Missing layout views pass through

A route can own children, slots, metadata, middleware, and lifecycle without rendering a layout shell. If layout.view is absent, the outlet passes through.

On this page