Skip to content

Advanced Patterns

Route guards

A guard runs before a route resolves. Return true to allow it, or a redirect() to send the visitor elsewhere. Because the redirect target is checked against your route map, a mistyped path fails at compile time rather than shipping a dead link.

router.ts
import { createRouter, redirect } from 'routekit';
const router = createRouter({
routes: {
'/dashboard': {
// A guard runs before the route resolves. Return a redirect to
// bounce unauthenticated users — fully type-checked against your
// route map, so a typo'd path is a compile error, not a 404.
guard: ({ session }) =>
session ? true : redirect('/login'),
component: () => import('./Dashboard'),
},
},
});

Parallel data loaders

Pair a lazy component with a loader and routekit fires both at once — the network request for data overlaps the network request for code, so a route that needs both is no slower than a route that needs either.

routes.ts
'/users/:id': {
// Loaders fire in parallel with the lazy component import, so data
// and code arrive together. The resolved value is typed and passed
// to the component as `data`.
loader: ({ params }) => fetchUser(params.id),
component: () => import('./User'),
}

Type-safe params

The path pattern is the type. routekit reads :id out of the string literal and requires it at every call site — no generated types, no codegen step, no drift between your routes and your navigation calls.

// routekit infers params from the path pattern at the type level.
router.navigate('/users/:id', { id: '42' }); // ok
router.navigate('/users/:id', {}); // TS error: missing 'id'
router.navigate('/uesrs/:id', { id: '42' }); // TS error: no such route

Nested layouts

Compose layouts by nesting routers. A parent router owns the chrome (sidebar, header); each child router swaps only its own outlet. Because routers are plain objects, you can split them across files and lazy-load an entire section's router the first time a visitor enters it.

  1. Define a child router for the section and export it.
  2. Mount it at the parent route's component as a lazy import.
  3. The child renders into the parent's outlet; URLs compose automatically.

Testing routes

routekit/testing ships a memory router — no DOM, no jsdom history shims. Drive navigation directly and assert on router.current. Guards, loaders, and redirects all run exactly as they do in the browser.

dashboard.test.ts
import { createMemoryRouter } from 'routekit/testing';
test('redirects guests away from /dashboard', async () => {
const router = createMemoryRouter(routes, { session: null });
await router.navigate('/dashboard');
expect(router.current.path).toBe('/login');
});