Reactividad
Guarda los hechos en Signals, deriva con Memos y sincroniza el mundo exterior con Effects.
Tres primitivas
Una fuente de verdad escribible. Lee con get(), escribe con set(v); leer registra una dependencia en el cálculo actual.
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.
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.
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 },
));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.
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.
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
signal.get()Lee y registra una dependencia en el Memo / Effect actualsignal.peek()Lee sin suscribirse; útil para instantáneas dentro de handlerssignal.set(v)Escribe y notifica a los suscriptoressignal.update(fn)Calcula el siguiente valor a partir del anterior y lo escribescope.createEffect(ctx, fn)Ejecuta un efecto secundario al cambiar; se libera con su Scope// 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.