Skip to main content
The Vertz router provides type-safe, file-system-free routing with loaders, nested layouts, and search param parsing. Route patterns are inferred from your route map, so typos in navigate() calls and missing params are caught at compile time.

defineRoutes()

Define your route map. The const modifier preserves literal path types for type-safe navigation.
import { defineRoutes } from '@vertz/ui/router';

const routes = defineRoutes({
  '/': {
    component: () => HomePage(),
  },
  '/tasks': {
    component: () => TaskListPage(),
    loader: async () => {
      const res = await taskApi.list();
      return res.ok ? res.data : [];
    },
  },
  '/tasks/:id': {
    component: () => TaskDetailPage(),
    params: { id: (v) => Number(v) },
  },
});

Signature

function defineRoutes<const T extends Record<string, RouteConfigLike>>(map: T): TypedRoutes<T>;

RouteConfig

PropertyTypeDescription
component() => Node | Promise<{ default: () => Node }>Component to render (supports lazy imports)
loader(ctx: { params, signal }) => Promise<T> | TData loader — runs before the component renders
errorComponent(error: Error) => NodeFallback component when loader fails
paramsParamSchemaParse and validate route params (:idnumber)
searchParamsSearchParamSchemaParse and validate search/query params
childrenRouteDefinitionMapNested routes (rendered via Outlet)
prerenderbooleanPre-render this route at build time (true = SSG, false = skip)
generateParams() => Record<string, string>[] | Promise<Record<string, string>[]>Generate param combinations for pre-rendering dynamic routes

createRouter()

Create a router instance from a route definition.
import { createRouter, defineRoutes } from '@vertz/ui/router';

const routes = defineRoutes({
  /* ... */
});
const router = createRouter(routes);

Signature

function createRouter<T extends Record<string, RouteConfigLike>>(
  routes: TypedRoutes<T>,
  initialUrl?: string,
  options?: RouterOptions,
): Router<T>;

Parameters

ParameterTypeDescription
routesTypedRoutes<T>Compiled route list from defineRoutes()
initialUrlstringOverride URL (used in SSR to set the server request URL)
optionsRouterOptionsConfiguration for server navigation and prefetching

RouterOptions

OptionTypeDefaultDescription
serverNavboolean | { timeout?: number }falseEnable server-side navigation (SSR prefetch on navigate)

Router<T>

PropertyTypeDescription
currentRouteMatch | nullCurrent matched route (reactive)
loaderDataunknown[]Data from the matched route’s loader (reactive)
loaderErrorError | nullError from the loader, if any (reactive)
searchParamsRecord<string, unknown>Parsed search params (reactive)
navigate(input)<TPath extends keyof T & string>(input: { to: TPath, ... }) => Promise<void>Navigate to a route pattern with typed params
revalidate()() => Promise<void>Re-run the current route’s loader
dispose()() => voidClean up the router

RouterView

Renders the component matched by the current route.
import { createRouter, defineRoutes, RouterView } from '@vertz/ui/router';

const routes = defineRoutes({
  /* ... */
});
const router = createRouter(routes);

function App() {
  return (
    <div>
      <nav>...</nav>
      <RouterView router={router} fallback={() => <div>Page not found</div>} />
    </div>
  );
}

Props

PropTypeDescription
routerRouterRouter instance
fallback() => NodeComponent to render when no route matches

Outlet

Renders nested child routes inside a layout component. Must be used within a component rendered by RouterView.
const routes = defineRoutes({
  '/dashboard': {
    component: () => DashboardLayout(),
    children: {
      '/': { component: () => DashboardHome() },
      '/settings': { component: () => DashboardSettings() },
    },
  },
});

function DashboardLayout() {
  return (
    <div className={styles.layout}>
      <Sidebar />
      <main>
        <Outlet />
      </main>
    </div>
  );
}

useRouter()

Access the router instance from any component in the tree. Must be called within RouterContext.Provider (set up automatically by RouterView).
import { useRouter } from '@vertz/ui/router';

function TaskCard({ taskId }: { taskId: string }) {
  const router = useRouter();

  const handleClick = () => {
    router.navigate({ to: '/tasks/:id', params: { id: taskId } });
  };

  return <div onClick={handleClick}>...</div>;
}

Signature

function useRouter<T extends Record<string, RouteConfigLike>>(): UnwrapSignals<Router<T>>;
In scaffolded apps, codegen writes .vertz/generated/router.d.ts, which augments useRouter() with your app’s route map so navigate() is typed by default. You can still pass a generic manually in non-generated setups.

useParams()

Read the current route’s parsed parameters.
import { useParams } from '@vertz/ui/router';

function TaskDetailPage() {
  const { id } = useParams<'/tasks/:id'>();
  // id is typed based on the path pattern
  return <div>Task {id}</div>;
}

Signature

function useParams<TPath extends string>(): ExtractParams<TPath>;
When a params schema is defined on the route config, useParams() returns the parsed values (e.g., id as number instead of string).

useSearchParams()

Read and write the current route’s search (query) params as a reactive proxy. Reads trigger signal tracking; writes batch-navigate to update the URL.
import { useSearchParams } from '@vertz/ui/router';

function SearchPage() {
  const sp = useSearchParams();

  // Read: reactive — re-renders when URL changes
  const query = sp.q;
  const page = sp.page;

  // Write: batched — multiple writes = single URL update
  sp.q = 'dragon';
  sp.page = 2;

  // Explicit navigate with push (creates history entry)
  sp.navigate({ sort: 'price' }, { push: true });

  // Remove a param
  delete sp.sort;
}

With a search params schema

Define a schema on the route to get typed, coerced values:
import { s } from '@vertz/schema';
import { defineRoutes } from '@vertz/ui/router';

const routes = defineRoutes({
  '/search': {
    component: () => SearchPage(),
    searchParams: s.object({
      q: s.string().default(''),
      page: s.coerce.number().default(1),
    }),
  },
});

function SearchPage() {
  // With route path generic: sp.q is string, sp.page is number
  const sp = useSearchParams<'/search'>();
}

Signatures

// Route path generic — infers types from route schema
function useSearchParams<TPath extends string>(): ReactiveSearchParams<ExtractSearchParams<TPath>>;

// Explicit type assertion
function useSearchParams<T extends Record<string, unknown>>(): ReactiveSearchParams<T>;

// No generic — returns Record<string, string>
function useSearchParams(): ReactiveSearchParams<Record<string, string>>;

ReactiveSearchParams

Property/MethodTypeDescription
sp[key]unknownRead a search param (reactive)
sp[key] = valueWrite a search param (batched, replaces URL)
delete sp[key]Remove a search param
sp.navigate(partial, opts?)voidMerge params and navigate. { push: true } creates history entry
Object.keys(sp)string[]Current param names
{ ...sp }objectSnapshot of current params

SSR behavior

During SSR, reads return the values from the request URL. Writes throw an error in development to catch accidental server-side mutations.
Create a Link component factory bound to a router’s state. Used internally — most apps use the Link component via RouterView setup.
import { createLink } from '@vertz/ui/router';

const Link = createLink(router.current, (url) =>
  router.navigate({ to: url as Parameters<typeof router.navigate>[0]['to'] }),
);

LinkProps

PropTypeDescription
hrefstringTarget URL
childrenstring | (() => Node)Link content
activeClassstringCSS class applied when the link matches the current route
classNamestringBase CSS class
prefetch'hover'Prefetch route data on hover

Types

interface RouteMatch {
  params: Record<string, string>;
  parsedParams?: Record<string, unknown>;
  route: CompiledRoute;
  matched: MatchedRoute[];
  searchParams: URLSearchParams;
  search: Record<string, unknown>;
}

interface NavigateOptions {
  replace?: boolean;
  params?: Record<string, string>;
  search?: string | URLSearchParams | Record<string, string | number | boolean>;
}

type NavigateInput<TPath extends string> = {
  to: TPath;
} & NavigateOptions;

type RoutePattern<T extends Record<string, RouteConfigLike>> = keyof T & string;
type RoutePaths<T extends Record<string, RouteConfigLike>> = /* concrete URL union */;
type ExtractParams<TPath extends string> = /* extracted param object */;
type ExtractSearchParams<TPath extends string, TMap = RouteDefinitionMap> =
  /* search param schema output, or Record<string, string> */;
type InferRouteMap<T> = T extends TypedRoutes<infer R> ? R : T;
type TypedRoutes<T> = CompiledRoute[] & { readonly __routes: T };

interface ReactiveSearchParams<T = Record<string, unknown>> {
  navigate(partial: Partial<T>, options?: { push?: boolean }): void;
  [key: string]: unknown;
}