docs/guide/reactivity
Conceptos clave · Data flow

Reactividad

Guarda los hechos en Signals, deriva con Memos y sincroniza el mundo exterior con Effects.

10 min de lectura

Tres primitivas

01
Signal

Una fuente de verdad escribible. Lee con get(), escribe con set(v); leer registra una dependencia en el cálculo actual.

02
Memo

Se calcula de forma perezosa a partir de otros valores reactivos; reutiliza su último resultado mientras las entradas no cambian y no notifica nada si el valor resulta igual.

03
Effect

Ejecuta un efecto secundario cuando cambian sus dependencias: actualizar un nodo, escribir un log, sincronizar un sistema externo.

Flujo de datos completo

ui.textFmt se suscribe a los Signals / Memos que le pasas y vuelve a formatear solo ese nodo de texto cuando cambian. Un Memo recibe sus dependencias a través de un struct de contexto explícito.

counter.zig
const count = try scope.createSignal(u32, 0);

const doubled = try scope.createMemo(u32, .{ .count = count }, struct {
    fn compute(ctx: anytype) u32 {
        return ctx.count.get() * 2;
    }
}.compute);

try root.appendChild(cx.allocator, try ui.textFmt(
    cx,
    scope,
    "count = {d}, doubled = {d}",
    .{ count, doubled },
    .{ .color = cx.tokens.color.fg_primary },
));
DATA FLOW
Un set recorre las aristas de dependencia —Signal → Memo → textFmt— y solo el nodo de texto suscrito se marca render-dirty.

Signals, no diffing

zenit mantiene un árbol de UI real para layout, hit-testing y dibujo, pero las actualizaciones no siguen el camino “volver a ejecutar componentes → construir un árbol candidato → hacer diff de todo”. Los Memos y Effects que leen un Signal registran una dependencia; set notifica exactamente a esos suscriptores, que marcan los nodos afectados como layout- o render-dirty.

SIGNAL vs DIFF
El árbol sigue ahí; lo que desaparece es reconstruir un árbol candidato y buscar diferencias en cada cambio.
La Reactive Counter.app real: cada +1 actualiza count y doubled a la vez hasta 3 / 6, y luego Reset pone a cero ambos suscriptores.

Actualizar desde eventos

Los callbacks de los componentes son HandlerRef. Guarda con cx.bindState un pequeño struct que tenga el puntero al Signal y convierte uno de sus métodos en callback con cx.on.

handlers.zig
const Bindings = struct {
    count: *ui.Signal(u32),

    fn increment(self: *Bindings) void {
        self.count.set(self.count.get() + 1);
    }
};

const bindings = try cx.bindState(Bindings, .{ .count = count });
const on_click = cx.on(Bindings, bindings, Bindings.increment);

const plus = try ui.widgets.Button(.{
    .label = "+1",
    .on_click = on_click,
}).mount(scope, cx);

Leer y suscribirse

APIQué hace
signal.get()Lee y registra una dependencia en el Memo / Effect actual
signal.peek()Lee sin suscribirse; útil para instantáneas dentro de handlers
signal.set(v)Escribe y notifica a los suscriptores
signal.update(fn)Calcula el siguiente valor a partir del anterior y lo escribe
scope.createEffect(ctx, fn)Ejecuta un efecto secundario al cambiar; se libera con su Scope
effects.zig
// Effect: sync a value to something outside the reactive graph.
try scope.createEffect(.{ .count = count }, struct {
    fn run(ctx: anytype) void {
        std.log.info("count is now {d}", .{ctx.count.get()});
    }
}.run);

// peek(): read without subscribing (no dependency is recorded).
const snapshot = count.peek();

// update(): read-modify-write in one call.
count.update(struct {
    fn inc(v: u32) u32 {
        return v + 1;
    }
}.inc);

Reglas prácticas

  • ✓

    Un hecho, un Signal. No guardes el mismo estado a la vez en un campo normal y en un Signal.

  • ✓

    Los valores derivables son Memos. No sincronices a mano campos derivados en cada handler.

  • ✓

    Los Effects solo sincronizan. Nunca escribas incondicionalmente en una dependencia del propio Effect: eso es un bucle.

  • ✓

    Los ciclos de vida pertenecen a un Scope. Cuando se libera el Scope de una página, sus Signals, Memos y Effects se van con él.

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