---
title: "Estado y eventos — Docs de zenit Zig UI"
description: "Deja que el Cx sea dueño del estado que debe sobrevivir entre frames, y responde a la entrada del usuario con handlers con tipos seguros."
url: https://zenit.z.express/es/docs/guide/state-events
language: es
alternate_en: https://zenit.z.express/docs/guide/state-events.md
alternate_zh: https://zenit.z.express/zh/docs/guide/state-events.md
alternate_ja: https://zenit.z.express/ja/docs/guide/state-events.md
alternate_ko: https://zenit.z.express/ko/docs/guide/state-events.md
alternate_fr: https://zenit.z.express/fr/docs/guide/state-events.md
alternate_de: https://zenit.z.express/de/docs/guide/state-events.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# Estado y eventos

Deja que el Cx sea dueño del estado que debe sobrevivir entre frames, y responde a la entrada del usuario con handlers con tipos seguros.

## Estado vinculado

`cx.bindState(T, init)` crea un `T` en el almacén de estado del Cx y devuelve un `*T` que sigue siendo válido entre frames. Luego `cx.on` convierte uno de sus métodos en un `HandlerRef` que acepta cualquier componente — sin IDs de estado asignados a mano.

`counter.zig`

```zig
const Counter = struct {
    allocator: std.mem.Allocator,
    value: u32 = 0,
    label: ?*ui.Node = null,
    buffer: [32]u8 = undefined,

    fn increment(self: *Counter) void {
        self.value += 1;
        const label = self.label orelse return;
        const text = std.fmt.bufPrint(
            &self.buffer,
            "Clicked {d} times",
            .{self.value},
        ) catch return;
        // Copies the text, marks it owned, frees the previous owned copy,
        // and marks the node dirty only if the text actually changed.
        label.setTextContent(self.allocator, text) catch return;
    }
};

const counter = try cx.bindState(Counter, .{ .allocator = cx.allocator });
const on_click = cx.on(Counter, counter, Counter.increment);
```

Cambia el texto desde un callback con `node.setTextContent(allocator, text)`: copia el contenido y lo marca como owned, libera la copia owned anterior y solo marca el nodo como sucio si el texto cambió de verdad — sin `markRenderDirty` manual. Por eso el struct guarda un campo `allocator`.

> NOTE
> 
> **¿Anónimo o direccionable?** `bindState` crea una entrada nueva en cada llamada, con un ID anónimo asignado según el orden de llamada que solo tiene sentido dentro de este montaje. Si necesitas recuperar el mismo estado por ID en otro sitio o en un montaje posterior, usa `cx.state(T, id, init)` con un ID explícito que elijas tú.

## Conectar componentes

Todos los campos de callback de los componentes son `?HandlerRef`. Pasa el `on_click` de arriba a un [Button](https://zenit.z.express/es/components/button) y guarda en el struct de estado el nodo de texto que quieras actualizar.

`view.zig`

```zig
const label = try ui.text(cx, "Clicked 0 times", .{
    .color = cx.tokens.color.fg_primary,
});
counter.label = label;

const button = try ui.widgets.Button(.{
    .label = "Increment",
    .variant = .primary,
    .on_click = on_click,
}).mount(scope, cx);

try root.appendChild(cx.allocator, button);
try root.appendChild(cx.allocator, label);
```

Los callbacks que llevan un valor — el estado marcado de un [Checkbox](https://zenit.z.express/es/components/checkbox) o Switch, el texto o el id de Input o Tabs — usan un constructor que transporta valor, como `ui.Cx.boolHandlerFrom`.

`prefs.zig`

```zig
const Prefs = struct {
    notify: bool = false,

    fn setNotify(self: *Prefs, checked: bool) void {
        self.notify = checked;
    }
};

const prefs = try cx.bindState(Prefs, .{});

const notify = try ui.widgets.Checkbox(.{
    .label_text = "Notify me",
    .on_change = ui.Cx.boolHandlerFrom(Prefs, prefs, Prefs.setNotify),
}).mount(scope, cx);
```

[Video](https://zenit.z.express/media/stories/checkbox.mp4?v=079ee6a961)

El harness hace clic en el área de impacto real del Checkbox: el aspecto marcado, el callback on\_change y el texto de estado se actualizan en el mismo pipeline de frame.

## El recorrido de un clic

La plataforma entrega pulsaciones y liberaciones, no “clics”. El hit-testing recorre los hijos de delante hacia atrás (orden Z) para encontrar el nodo superior; cuando la pulsación y la liberación caen sobre el mismo elemento, el `EventDispatcher` sintetiza un `click`, lo despacha por capture → target → bubble, llama al `HandlerRef` `on_click` del nodo y termina en el método que vinculaste.

EVENT ROUTING

Un HandlerRef es solo “callback + puntero de contexto”. El callback que genera cx.on convierte el contexto de vuelta a \*Counter y llama al método; el estado en sí permanece en el almacén de estado del Cx.

| EventResult | Significado |
| --- | --- |
| `.ignored` | No gestionado; sigue propagándose y permite el comportamiento por defecto |
| `.handled` | Gestionado; sigue propagándose pero impide el comportamiento por defecto |
| `.stop` | Gestionado; detiene la propagación |

## Construir handlers

| Constructor | Firma del método | Uso |
| --- | --- | --- |
| `cx.on(State, s, State.m)` | `fn (*State) void` | Clics de botón y otros eventos de “ha ocurrido” |
| `ui.Cx.boolHandlerFrom(…)` | `fn (*State, bool) void` | Checkbox, Switch, Accordion |
| `ui.Cx.strHandlerFrom(…)` | `fn (*State, []const u8) void` | Input, Textarea, Tabs, Radio; el slice solo es válido durante la llamada |
| `ui.clickable(node, handler)` | — | Añadir on\_click a cualquier nodo |

## Eventos de bajo nivel

Los botones, los campos de entrada y compañía ya encapsulan las interacciones habituales. Recurre a `ui.events.Event` solo para interacciones personalizadas: cubre el ratón (down, up, click, double\_click, movimiento, entrada/salida, scroll, magnify, arrastre de archivos), el teclado, `text_input`, `ime_preedit` / `ime_commit` y el foco. Prefiere `ui.actions` para los comandos de teclado y `ui.focus` para mover el foco.

| Necesidad | API preferida |
| --- | --- |
| Clic, escritura, selección | `ui.widgets.* callbacks` |
| Hover, anillo de foco | `ui.hooks.useHover / useFocusRing` |
| Atajos y comandos | `ui.actions` |
| Arrastre, hover de rango | `ui.interaction.drag / range_hover` |
| Teclas/ratón en bruto, scroll, IME | `ui.events.Event` |

## Ciclo de vida y limpieza

El estado vinculado pertenece al **Cx** y se libera cuando el Cx hace deinit. Si `T` declara un `deinit(*T)` **pub**, el framework lo llama justo antes de liberarlo — pon ahí la limpieza de tus ArrayList, HashMap y buffers. Los Signals, Memos, Effects y callbacks de `onCleanup` pertenecen a un **Scope** y desaparecen con `scope.dispose()`.

OWNERSHIP

Dos ciclos de vida: a nivel de Cx (toda la ventana / app) y a nivel de Scope (una página o panel).

`editor_state.zig`

```zig
const EditorState = struct {
    allocator: std.mem.Allocator,
    lines: std.ArrayList([]u8) = .{},

    // Must be pub: the state store detects it and calls it when the Cx deinits.
    pub fn deinit(self: *EditorState) void {
        for (self.lines.items) |line| self.allocator.free(line);
        self.lines.deinit(self.allocator);
    }
};

// The initial value must not own resources yet; allocate after binding.
const editor = try cx.bindState(EditorState, .{ .allocator = cx.allocator });

// Do NOT also call scope.onCleanup(EditorState, editor, EditorState.deinit):
// the framework already calls deinit, so that would free everything twice.
```

> WARNING
> 
> **No hagas deinit dos veces.** Documentación antigua sugería registrar también `scope.onCleanup(T, state, T.deinit)` para el estado vinculado. Si `deinit` es pub, el framework ya lo llama; registrarlo otra vez libera todo dos veces.

Usa `scope.onCleanup` solo cuando la limpieza no sea un `deinit` pub, o cuando un recurso deba seguir a un Scope (una página) en lugar de a todo el Cx.

`page_cache.zig`

```zig
// A resource that must follow the page (Scope), not the whole Cx.
const PageCache = struct {
    allocator: std.mem.Allocator,
    hits: std.StringHashMapUnmanaged(u32) = .{},

    // Not named deinit, so the state store will not call it again.
    fn release(self: *PageCache) void {
        self.hits.deinit(self.allocator);
        self.hits = .{};
    }
};

const cache = try cx.bindState(PageCache, .{ .allocator = cx.allocator });
try scope.onCleanup(PageCache, cache, PageCache.release);
```

-   **Los valores iniciales no poseen nada.** Pasa a `bindState` un valor cero o solo campos prestados (punteros, un allocator); asigna memoria cuando ya tengas el `*T`.
    
-   **Guarda el allocator como campo.** `deinit(*T)` recibe un solo argumento, así que el allocator usado para liberar tiene que vivir en el struct.
    
-   **No retengas nodos muertos.** Un puntero a Node solo es válido mientras vivan su árbol y su Scope; después de que una página se desmonte o se reconstruya, no accedas a nodos antiguos a través del estado.
