docs/advanced/routing
Capacidades · Navigation

Enrutamiento dentro de la app

Describe las páginas como un enum, expón la página actual y sus parámetros como dos Signals y obtén atrás / adelante de una pila de historial de capacidad fija.

8 min de lectura

El modelo de Router

ui.fx.Router(Page) es un router en memoria guiado por un enum, no un router de URL web. Expone dos Signals, current: *Signal(Page) y params: *Signal(RouteParams), y mantiene una pila de historial de hasta 64 entradas.

app.zig
const std = @import("std");
const ui = @import("ui");

const Page = enum { welcome, editor, settings };
const Router = ui.fx.Router(Page);

fn mountApp(cx: *ui.Cx, scope: *ui.Scope) anyerror!*ui.Node {
    // The router's two Signals are created in `scope`, so pass a scope that
    // lives as long as the window. The router value itself is large (~290 KB of
    // inline history), so keep it in Cx-owned state and pass the pointer around.
    const router = try cx.bindState(Router, try Router.init(scope, .welcome));

    const shell = try ui.box(cx, .{
        .width = .fill(),
        .height = .fill(),
        .direction = .column,
    }, .{});
    // ...append a toolbar and the page host (see "Rendering pages")
    _ = router;
    return shell;
}
navigate.zig
var params: ui.fx.router.RouteParams = .{};
try params.set("file", "draft.md"); // copies the bytes; errors on overflow

router.navigate(.editor, params); // page + params
router.navigateTo(.settings); // page, empty params

_ = router.back(); // false when already at the oldest entry
_ = router.forward(); // false when there is no forward branch

if (router.canGoBack()) {
    // e.g. enable a Back button
}

RouteParams.set copia los bytes de la clave y el valor en sus propios búferes, así que los cambios posteriores en el búfer de quien llama no le afectan; asignar una clave existente sobrescribe su valor. La capacidad es fija y el desbordamiento devuelve un error en lugar de truncar:

LímiteValorAl desbordar
Parámetros por entradamax_params = 8error.TooManyParams
Longitud de la clavemax_key_len = 64error.KeyTooLong
Longitud del valormax_value_len = 512error.ValueTooLong

navigate, back y forward escriben en ambos Signals. canGoBack() / canGoForward() son lecturas simples, no Signals. Para controlar el router desde un handler, guarda el puntero en un struct de cx.bindState:

back_button.zig
const Nav = struct {
    router: *Router,

    fn goBack(self: *Nav) void {
        _ = self.router.back();
    }
};

const nav = try cx.bindState(Nav, .{ .router = router });
const back_button = try ui.widgets.Button(.{
    .label = "Back",
    .on_click = cx.on(Nav, nav, Nav.goBack),
}).mount(scope, cx);

Renderizar páginas

La página actual es un Signal(Page); cambia los subárboles de página según su valor con ui.Match. Match da a cada página un Scope hijo; al cambiar, primero hace dispose del Scope anterior (sus Signals, Effects y recursos se van con él), luego libera los nodos antiguos y monta la nueva página.

pages.zig
const page_host = try ui.box(cx, .{
    .width = .fill(),
    .height = .fill(),
}, .{});

// Match appends the page to page_host and rebuilds it when router.current
// changes; each page gets a fresh child Scope that is disposed on leave.
_ = try ui.Match(Page, scope, page_host, router.current, cx, struct {
    fn build(child_scope: *ui.Scope, c: *ui.Cx, page: Page) anyerror!*ui.Node {
        return switch (page) {
            .welcome => mountWelcome(c, child_scope),
            .editor => mountEditor(c, child_scope),
            .settings => mountSettings(c, child_scope),
        };
    }
}.build);

Match solo reconstruye cuando cambia el valor del enum: navegar de .editor a .editor con otros parámetros mantiene la página montada. Una página que depende de los parámetros debe suscribirse ella misma a router.params:

editor_page.zig
// editor -> editor with other params does not rebuild the page,
// so subscribe to the params Signal inside the page.
try scope.createEffect(.{ .params = router.params }, struct {
    fn run(ctx: anytype) void {
        // get() returns a copy; keep it in a local so the slices stay valid.
        const params = ctx.params.get();
        const file = params.get("file") orelse "untitled.md";
        std.log.info("editor shows {s}", .{file});
    }
}.run);

Reglas del historial

HISTORY STACK
back / forward solo mueven el cursor; un navigate después de back primero corta history_len en el cursor y luego apila la nueva entrada.
  • ✓

    Una nueva navegación trunca la rama hacia adelante. Navegar después de back descarta las entradas forward anteriores.

  • ✓

    Cada navigate apila. La misma página con otros parámetros — o incluso un destino idéntico — se convierte en una nueva entrada.

  • ✓

    Con 64 entradas, se descarta la mitad más antigua. Antes del apilado número 65 se conservan las 32 más recientes y el cursor se desplaza con ellas.

  • ✓

    Sin URLs. Los deep links y los URL schemes del sistema debe mapearlos la app a navigate.

CAPACITY 64
El descarte es por lotes: en lugar de expulsar una entrada por apilado, libera la mitad de la capacidad de una vez.

Registrar la navegación

El router no registra nada por sí mismo. Para depurar la navegación, escribe las transiciones relevantes en la Console del Cx actual bajo un scope router; la Console de DevTools podrá filtrar por scope y mostrar el nivel y el punto de llamada.

trace.zig
router.navigate(.editor, params);
cx.console().scoped("router").debug("navigate -> {s}", .{@tagName(Page.editor)});

Consulta DevTools para la API completa de Console.

zenit · Doble licenciaGratis para proyectos de código abierto bajo GPL-3.0-only; los productos cerrados o comerciales necesitan una licencia comercial.Contacta con el autor: zongyi.xzy#gmail.com (cambia # por @)zenit 5f9add5+wip 2026-09-30