---
title: "État et événements — Docs zenit Zig UI"
description: "Confiez au Cx l'état qui doit survivre d'une frame à l'autre, et répondez aux entrées utilisateur avec des handlers typés."
url: https://zenit.z.express/fr/docs/guide/state-events
language: fr
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_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
---

# État et événements

Confiez au Cx l'état qui doit survivre d'une frame à l'autre, et répondez aux entrées utilisateur avec des handlers typés.

## État lié

`cx.bindState(T, init)` crée un `T` dans le magasin d'état du Cx et renvoie un `*T` qui reste valide d'une frame à l'autre. `cx.on` transforme ensuite l'une de ses méthodes en un `HandlerRef` accepté par n'importe quel composant — sans ID d'état attribué à la main.

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

Remplacez le texte depuis un callback avec `node.setTextContent(allocator, text)` : il copie le contenu et le marque owned, libère la copie owned précédente, et ne marque le nœud comme sale que si le texte a réellement changé — pas de `markRenderDirty` manuel. C'est pourquoi le struct conserve un champ `allocator`.

> NOTE
> 
> **Anonyme ou adressable ?** `bindState` crée une nouvelle entrée à chaque appel, avec un ID anonyme attribué dans l'ordre des appels qui n'a de sens qu'au sein de ce montage. Si vous devez récupérer le même état par ID ailleurs ou lors d'un montage ultérieur, utilisez `cx.state(T, id, init)` avec un ID explicite de votre choix.

## Brancher les composants

Les champs de callback des composants sont tous des `?HandlerRef`. Passez le `on_click` obtenu plus haut à un [Button](https://zenit.z.express/fr/components/button), et stockez dans le struct d'état le nœud texte à mettre à jour.

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

Les callbacks porteurs d'une valeur — l'état coché d'un [Checkbox](https://zenit.z.express/fr/components/checkbox) ou d'un Switch, le texte ou l'id d'un Input ou de Tabs — utilisent un constructeur qui transmet la valeur, comme `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)

Le harness clique sur la vraie zone de clic du Checkbox : l'aspect coché, le callback on\_change et le texte d'état se mettent à jour dans le même pipeline de frame.

## Le trajet d'un clic

La plateforme transmet des appuis et des relâchements, pas des « clics ». Le hit-testing parcourt les enfants de l'avant vers l'arrière (ordre Z) pour trouver le nœud le plus haut ; quand l'appui et le relâchement tombent sur le même élément, l'`EventDispatcher` synthétise un `click`, le distribue via capture → target → bubble, appelle le `HandlerRef` `on_click` du nœud et aboutit dans la méthode que vous avez liée.

EVENT ROUTING

Un HandlerRef n'est qu'un « callback + pointeur de contexte ». Le callback construit par cx.on reconvertit le contexte en \*Counter et appelle la méthode ; l'état lui-même reste dans le magasin d'état du Cx.

| EventResult | Signification |
| --- | --- |
| `.ignored` | Non traité ; la propagation continue et le comportement par défaut est permis |
| `.handled` | Traité ; la propagation continue mais le comportement par défaut est empêché |
| `.stop` | Traité ; la propagation s'arrête |

## Construire des handlers

| Constructeur | Signature de méthode | Usage |
| --- | --- | --- |
| `cx.on(State, s, State.m)` | `fn (*State) void` | Clics de bouton et autres événements « c'est arrivé » |
| `ui.Cx.boolHandlerFrom(…)` | `fn (*State, bool) void` | Checkbox, Switch, Accordion |
| `ui.Cx.strHandlerFrom(…)` | `fn (*State, []const u8) void` | Input, Textarea, Tabs, Radio ; le slice n'est valide que pendant l'appel |
| `ui.clickable(node, handler)` | — | Attacher on\_click à n'importe quel nœud |

## Événements de bas niveau

Les boutons, champs de saisie et consorts encapsulent déjà les interactions courantes. N'utilisez `ui.events.Event` que pour une interaction personnalisée : il couvre la souris (down, up, click, double\_click, déplacement, entrée/sortie, défilement, magnify, glisser de fichiers), le clavier, `text_input`, `ime_preedit` / `ime_commit` et le focus. Préférez `ui.actions` pour les commandes clavier et `ui.focus` pour déplacer le focus.

| Besoin | API recommandée |
| --- | --- |
| Clic, saisie, sélection | `ui.widgets.* callbacks` |
| Survol, anneau de focus | `ui.hooks.useHover / useFocusRing` |
| Raccourcis et commandes | `ui.actions` |
| Glisser, survol de plage | `ui.interaction.drag / range_hover` |
| Touches/souris brutes, défilement, IME | `ui.events.Event` |

## Durée de vie et nettoyage

L'état lié appartient au **Cx** et est libéré lors du deinit du Cx. Si `T` déclare un `deinit(*T)` **pub**, le framework l'appelle juste avant la libération — placez-y le nettoyage de vos ArrayList, HashMap et buffers. Les Signals, Memos, Effects et callbacks `onCleanup` appartiennent à un **Scope** et disparaissent avec `scope.dispose()`.

OWNERSHIP

Deux durées de vie : au niveau du Cx (toute la fenêtre / l'app) et au niveau du Scope (une page ou un panneau).

`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
> 
> **Ne faites pas deinit deux fois.** D'anciennes docs suggéraient d'enregistrer aussi `scope.onCleanup(T, state, T.deinit)` pour l'état lié. Si `deinit` est pub, le framework l'appelle déjà ; l'enregistrer à nouveau libère tout deux fois.

N'utilisez `scope.onCleanup` que lorsque le nettoyage n'est pas un `deinit` pub, ou lorsqu'une ressource doit suivre un Scope (une page) plutôt que tout le 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);
```

-   **Les valeurs initiales ne possèdent rien.** Passez à `bindState` une valeur zéro ou uniquement des champs empruntés (pointeurs, un allocator) ; allouez une fois que vous avez le `*T`.
    
-   **Gardez l'allocator dans un champ.** `deinit(*T)` ne prend qu'un argument, donc l'allocator servant à libérer doit vivre dans le struct.
    
-   **Ne gardez pas de nœuds morts.** Un pointeur de Node n'est valide que tant que son arbre et son Scope vivent ; après le démontage ou la reconstruction d'une page, n'accédez plus aux anciens nœuds via l'état.
