docs/guide/reactivity
Concepts clés · Data flow

Réactivité

Stockez les faits dans des Signals, dérivez avec des Memos et synchronisez le monde extérieur avec des Effects.

10 min de lecture

Trois primitives

01
Signal

Une source de vérité modifiable. Lisez avec get(), écrivez avec set(v) ; la lecture enregistre une dépendance dans le calcul en cours.

02
Memo

Calculé paresseusement à partir d'autres valeurs réactives ; réutilise son dernier résultat tant que les entrées ne changent pas et reste silencieux si la valeur est égale.

03
Effect

Exécute un effet de bord quand ses dépendances changent : mettre à jour un nœud, journaliser, synchroniser un système externe.

Flux de données de bout en bout

ui.textFmt s'abonne aux Signals / Memos que vous lui passez et ne reformate que ce nœud texte quand ils changent. Un Memo reçoit ses dépendances via un struct de contexte explicite.

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 parcourt les arêtes de dépendance — Signal → Memo → textFmt — et seul le nœud texte abonné est marqué render-dirty.

Des Signals, pas de diff

zenit conserve un vrai arbre UI pour le layout, le hit-testing et le dessin, mais les mises à jour ne passent pas par « ré-exécuter les composants → construire un arbre candidat → tout comparer ». Les Memos et Effects qui lisent un Signal enregistrent une dépendance ; set notifie exactement ces abonnés, qui marquent les nœuds concernés layout- ou render-dirty.

SIGNAL vs DIFF
L'arbre est toujours là ; ce qui disparaît, c'est la reconstruction d'un arbre candidat et la recherche de différences à chaque changement.
La vraie Reactive Counter.app : chaque +1 met à jour count et doubled ensemble jusqu'à 3 / 6, puis Reset remet les deux abonnés à zéro.

Mettre à jour depuis des événements

Les callbacks des composants sont des HandlerRef. Conservez avec cx.bindState un petit struct qui détient le pointeur du Signal, puis transformez l'une de ses méthodes en callback avec 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);

Lire et s'abonner

APIRôle
signal.get()Lit et enregistre une dépendance dans le Memo / Effect courant
signal.peek()Lit sans s'abonner — pratique pour un instantané dans un handler
signal.set(v)Écrit et notifie les abonnés
signal.update(fn)Calcule la valeur suivante à partir de la précédente, puis écrit
scope.createEffect(ctx, fn)Exécute un effet de bord au changement ; libéré avec son 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);

Règles pratiques

  • ✓

    Un fait, un Signal. Ne gardez pas le même état à la fois dans un champ ordinaire et dans un Signal.

  • ✓

    Les valeurs dérivables sont des Memos. Ne synchronisez pas à la main des champs dérivés dans chaque handler.

  • ✓

    Les Effects ne font que synchroniser. N'écrivez jamais inconditionnellement dans une dépendance de l'Effect lui-même : c'est une boucle.

  • ✓

    Les durées de vie appartiennent à un Scope. Quand le Scope d'une page est libéré, ses Signals, Memos et Effects partent avec lui.

zenit · Double licenceGratuit pour les projets open source sous GPL-3.0-only ; les produits propriétaires ou commerciaux nécessitent une licence commerciale.Contacter l’auteur : zongyi.xzy#gmail.com (remplacez # par @)zenit 5f9add5+wip 2026-09-30