Routing
Rezi's page router handles application-level screen navigation (Home, Logs, Settings, Detail) while the internal runtime router continues handling widget-level navigation (tabs, dropdowns, tree, vi...
Rezi's page router handles application-level screen navigation (Home, Logs, Settings, Detail) while the internal runtime router continues handling widget-level navigation (tabs, dropdowns, tree, virtual list).
Why Use Routing
Use the page router when your app needs:
- Multi-screen workflows
- Push/replace/back semantics
- Navigation history
- Global route keybindings
- Focus restoration when returning to previous screens
CreateApp Route Mode
createApp supports route mode directly:
import { createApp, ui, type RouteDefinition } from "@rezi-ui/core";
type State = { counter: number };
const routes: readonly RouteDefinition<State>[] = [
{
id: "home",
title: "Home",
keybinding: "ctrl+1",
screen: (_params, ctx) =>
ui.button({
id: "to-settings",
label: `Counter ${String(ctx.state.counter)}`,
onPress: () => ctx.router.navigate("settings"),
}),
},
{
id: "settings",
title: "Settings",
keybinding: "ctrl+2",
screen: (_params, ctx) =>
ui.button({
id: "inc",
label: "Increment",
onPress: () => ctx.update((prev) => ({ ...prev, counter: prev.counter + 1 })),
}),
},
];
const app = createApp({
backend,
initialState: { counter: 0 },
routes,
initialRoute: "home",
routeHistoryMaxDepth: 50,
});Notes:
- When routes are configured,
createAppmanages the widgetviewinternally. - Calling
app.view(...)in route mode throwsZRUI_MODE_CONFLICT.
Route Definition
type RouteDefinition<S = unknown> = {
id: string;
screen: (params: RouteParams, context: RouteRenderContext<S>) => VNode;
guard?: (params: RouteParams, state: Readonly<S>, ctx: RouteGuardContext) => RouteGuardResult;
children?: readonly RouteDefinition<S>[];
title?: string;
keybinding?: string;
};
type RouteParams = Readonly<Record<string, string>>;
type RouteGuardResult = true | false | { redirect: string; params?: RouteParams };
type RouteGuardContext = {
from: RouteLocation;
to: RouteLocation;
action: "navigate" | "replace" | "back";
};
type RouteRenderContext<S> = {
router: RouterApi;
state: Readonly<S>;
update: (updater: S | ((prev: Readonly<S>) => S)) => void;
outlet: VNode | null;
};idmust be unique.paramsare immutable string maps.guardruns before entering a route.childrenenables nested parent/child layouts.titleis used by route-aware UI helpers.keybindingregisters global navigation bindings.
Router API
When route mode is enabled, app.router is available:
interface RouterApi {
navigate(routeId: string, params?: RouteParams): void;
replace(routeId: string, params?: RouteParams): void;
back(): void;
currentRoute(): { id: string; params: RouteParams };
canGoBack(): boolean;
history(): readonly { id: string; params: RouteParams }[];
}Semantics:
navigate: push a new history entryreplace: replace current entry in-placeback: pop one entry when availablenavigateto the sameid+params: no-op
Route Guards
Use guard to block or redirect before navigation commits:
{
id: "admin",
title: "Admin",
guard: (_params, state) => {
if (!state.isAdmin) {
return { redirect: "home" };
}
return true;
},
screen: () => ui.text("Admin"),
}- Return
trueto allow. - Return
falseto cancel. - Return
{ redirect: "routeId" }to reroute before rendering the blocked target. - Guards run for
navigate,replace, andbackdestination resolution. - For nested routes, guards run in ancestry order (parent to child).
RouteGuardContext includes:
from: current route locationto: attempted destination locationaction:"navigate" | "replace" | "back"
Nested Routes and Outlet Rendering
Use children to define nested route trees:
const routes: readonly RouteDefinition<State>[] = [
{
id: "settings",
title: "Settings",
screen: (_params, ctx) =>
ui.column({ gap: 1 }, [
ui.text("Settings"),
ctx.outlet ?? ui.text("Select a tab"),
]),
children: [
{ id: "profile", title: "Profile", screen: () => ui.text("Profile") },
{ id: "appearance", title: "Appearance", screen: () => ui.text("Appearance") },
],
},
];- Child routes render through
context.outletin the parent screen. - Child route ids are still globally unique in the route tree.
- Route helpers (
routerBreadcrumb,routerTabs) resolve nested titles correctly. routerTabskeeps the top-level parent tab active when a nested child route is selected.
History Stack
- Default max depth:
50 - Configure via
routeHistoryMaxDepth - Bounded/circular behavior: overflow evicts oldest entries
- Public history entries are serializable (
id,params)
Keybinding Integration
Routes with keybinding register global app keybindings automatically.
Behavior:
- If keybinding target is not current route:
navigate(targetRoute) - If already current: no-op
Focus Restoration
On route transitions:
- Current route focus state is snapshotted before navigate/replace/back
back()restores the destination route focus snapshot when available
This preserves screen-level focus continuity for workflows like:
- Focus a control in Logs
- Navigate to Detail
- Back to Logs
- Focus returns to prior Logs control
Route UI Helpers
Use existing navigation widgets with router state:
ui.routerTabs(ctx.router, routes, { id: "main-tabs" });
ui.routerBreadcrumb(ctx.router, routes, { id: "main-crumb" });These are thin wrappers over built-in tabs and breadcrumb widgets.
routerTabs uses replace semantics by default so top-level tab switches
don't spam route history. If you explicitly want push history per tab switch:
ui.routerTabs(ctx.router, routes, {
id: "main-tabs",
historyMode: "push",
});Parameterized Routes
Pass params in navigate/replace:
ctx.router.navigate("detail", { id: entry.id });
ctx.router.replace("detail", { id: nextEntry.id });Read params in screen:
screen: (params, ctx) => {
const id = params["id"];
// ...
}JSX Usage
No special JSX router wrapper is required.
- Route screens can return JSX VNodes via
@rezi-ui/jsx - Router API is available through the screen render context (
ctx.router)
Record/Replay Compatibility
Router transitions are deterministic and state-driven:
- Same route action sequence produces the same route history
- History entries are serializable
- Back/replace/push semantics are stable across replay
Lifecycle & Updates
This guide explains how a Rezi app moves through its lifecycle, how updates are committed, and what “deterministic scheduling” means in practice.
Layout
Rezi uses a cell-based layout system: all sizes and coordinates are measured in terminal character cells (not pixels). This makes layout deterministic across platforms and terminal emulators.