Réactivité
Stockez les faits dans des Signals, dérivez avec des Memos et synchronisez le monde extérieur avec des Effects.
Trois primitives
Une source de vérité modifiable. Lisez avec get(), écrivez avec set(v) ; la lecture enregistre une dépendance dans le calcul en cours.
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.
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.
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 },
));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.
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.
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
signal.get()Lit et enregistre une dépendance dans le Memo / Effect courantsignal.peek()Lit sans s'abonner — pratique pour un instantané dans un handlersignal.set(v)Écrit et notifie les abonnéssignal.update(fn)Calcule la valeur suivante à partir de la précédente, puis écritscope.createEffect(ctx, fn)Exécute un effet de bord au changement ; libéré avec son 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);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.