---
title: "Zustand & Events — zenit Zig UI Doku"
description: "Überlassen Sie dem Cx den Zustand, der über Frames hinweg bestehen muss, und beantworten Sie Benutzereingaben mit typsicheren Handlern."
url: https://zenit.z.express/de/docs/guide/state-events
language: de
alternate_en: https://zenit.z.express/docs/guide/state-events.md
alternate_zh: https://zenit.z.express/zh/docs/guide/state-events.md
alternate_es: https://zenit.z.express/es/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
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# Zustand und Events

Überlassen Sie dem Cx den Zustand, der über Frames hinweg bestehen muss, und beantworten Sie Benutzereingaben mit typsicheren Handlern.

## Gebundener Zustand

`cx.bindState(T, init)` legt ein `T` im State-Store des Cx an und gibt einen `*T` zurück, der über Frames hinweg gültig bleibt. `cx.on` macht dann aus einer seiner Methoden eine `HandlerRef`, die jede Komponente akzeptiert — ohne von Hand vergebene State-IDs.

`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);
```

Tauschen Sie Text in einem Callback mit `node.setTextContent(allocator, text)` aus: Es kopiert den Inhalt und markiert ihn als owned, gibt die vorherige owned-Kopie frei und markiert den Node nur dann als dirty, wenn sich der Text tatsächlich geändert hat — kein manuelles `markRenderDirty`. Deshalb hält das Struct ein `allocator`\-Feld.

> NOTE
> 
> **Anonym oder adressierbar?** `bindState` legt bei jedem Aufruf einen neuen Eintrag an, mit einer anonymen, in Aufrufreihenfolge vergebenen ID, die nur innerhalb dieses Mounts etwas bedeutet. Wenn Sie denselben Zustand anderswo oder bei einem späteren Mount per ID abrufen müssen, verwenden Sie `cx.state(T, id, init)` mit einer selbst gewählten expliziten ID.

## Komponenten verdrahten

Die Callback-Felder von Komponenten sind alle `?HandlerRef`. Übergeben Sie das `on_click` von oben an einen [Button](https://zenit.z.express/de/components/button) und speichern Sie den zu aktualisierenden Text-Node im State-Struct.

`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);
```

Callbacks mit Wert — der Häkchen-Zustand einer [Checkbox](https://zenit.z.express/de/components/checkbox) oder eines Switch, Text oder id aus Input oder Tabs — verwenden einen wertübergebenden Konstruktor wie `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)

Der harness klickt auf den echten Trefferbereich der Checkbox: Häkchen-Optik, on\_change-Callback und Statustext aktualisieren sich in derselben Frame-Pipeline.

## Der Weg eines Klicks

Die Plattform liefert Drücken und Loslassen, keine „Klicks“. Das Hit-Testing durchläuft die Kinder von vorne nach hinten (Z-Reihenfolge), um den obersten Node zu finden; landen Drücken und Loslassen auf demselben Element, synthetisiert der `EventDispatcher` einen `click`, verteilt ihn über capture → target → bubble, ruft die `on_click`\-`HandlerRef` des Nodes auf und landet schließlich in der Methode, die Sie gebunden haben.

EVENT ROUTING

Eine HandlerRef ist nur „Callback + Kontextzeiger“. Der von cx.on erzeugte Callback castet den Kontext zurück zu \*Counter und ruft die Methode auf; der Zustand selbst bleibt im State-Store des Cx.

| EventResult | Bedeutung |
| --- | --- |
| `.ignored` | Nicht behandelt; Weiterleitung läuft, Standardverhalten erlaubt |
| `.handled` | Behandelt; Weiterleitung läuft, Standardverhalten wird verhindert |
| `.stop` | Behandelt; Weiterleitung stoppt |

## Handler erstellen

| Konstruktor | Methodensignatur | Einsatz |
| --- | --- | --- |
| `cx.on(State, s, State.m)` | `fn (*State) void` | Button-Klicks und andere „es ist passiert“-Events |
| `ui.Cx.boolHandlerFrom(…)` | `fn (*State, bool) void` | Checkbox, Switch, Accordion |
| `ui.Cx.strHandlerFrom(…)` | `fn (*State, []const u8) void` | Input, Textarea, Tabs, Radio; der Slice ist nur während des Aufrufs gültig |
| `ui.clickable(node, handler)` | — | on\_click an beliebigen Node hängen |

## Low-Level-Events

Buttons, Eingabefelder und Co. kapseln die üblichen Interaktionen bereits. Greifen Sie nur für eigene Interaktionen zu `ui.events.Event`: Es deckt die Maus (down, up, click, double\_click, Bewegung, Betreten/Verlassen, Scrollen, Magnify, Datei-Drag), die Tastatur, `text_input`, `ime_preedit` / `ime_commit` und den Fokus ab. Für Tastaturbefehle nutzen Sie bevorzugt `ui.actions`, zum Verschieben des Fokus `ui.focus`.

| Bedarf | Bevorzugte API |
| --- | --- |
| Klicken, tippen, auswählen | `ui.widgets.* callbacks` |
| Hover, Fokusring | `ui.hooks.useHover / useFocusRing` |
| Shortcuts und Befehle | `ui.actions` |
| Drag, Bereichs-Hover | `ui.interaction.drag / range_hover` |
| Rohe Tasten/Maus, Scrollen, IME | `ui.events.Event` |

## Lebensdauer und Aufräumen

Gebundener Zustand gehört dem **Cx** und wird beim deinit des Cx freigegeben. Deklariert `T` ein **pub** `deinit(*T)`, ruft das Framework es direkt vor dem Freigeben auf — dort gehört das Aufräumen Ihrer ArrayList, HashMap und Puffer hin. Signals, Memos, Effects und `onCleanup`\-Callbacks gehören zu einem **Scope** und verschwinden mit `scope.dispose()`.

OWNERSHIP

Zwei Lebensdauern: Cx-weit (das ganze Fenster / die App) und Scope-weit (eine Seite oder ein 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
> 
> **Kein doppeltes deinit.** Ältere Dokumentation empfahl, für gebundenen Zustand zusätzlich `scope.onCleanup(T, state, T.deinit)` zu registrieren. Ist `deinit` pub, ruft das Framework es bereits auf; eine erneute Registrierung gibt alles doppelt frei.

Verwenden Sie `scope.onCleanup` nur, wenn das Aufräumen kein pub `deinit` ist oder eine Ressource einem Scope (einer Seite) statt dem ganzen Cx folgen muss.

`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);
```

-   **Anfangswerte besitzen nichts.** Übergeben Sie `bindState` einen Nullwert oder nur geliehene Felder (Zeiger, einen Allocator); allozieren Sie erst, wenn Sie den `*T` haben.
    
-   **Halten Sie den Allocator als Feld.** `deinit(*T)` nimmt nur ein Argument, daher muss der Allocator zum Freigeben im Struct liegen.
    
-   **Halten Sie keine toten Nodes.** Ein Node-Zeiger ist nur gültig, solange sein Baum und sein Scope leben; nachdem eine Seite unmountet oder neu aufgebaut wurde, greifen Sie nicht über den Zustand auf alte Nodes zu.
