Reactivity

Verve's reactive core is three primitives — Signal, Effect, and Owner — shared verbatim between the server and the WASM client. There is no virtual-DOM diffing: a signal write touches exactly the DOM nodes that depend on it.

Signals

A Signal(T) holds a value. Reading it inside an effect subscribes the effect; writing notifies every subscriber:

const sig = try ctx.useSignal(i32, 0);

sig.get();      // read AND subscribe the current effect
sig.peek();     // read without tracking
sig.set(42);    // write + notify (no-op if value unchanged)
sig.increment() // numeric convenience (also: decrement)

set skips notification when the new value equals the old (std.meta.eql), so spurious re-runs never happen.

Effects

createEffect runs a function eagerly once to collect dependencies, then re-runs it whenever any signal it read changes:

const Watcher = struct {
    count: *verve.Signal(i32),
    fn run(self: *@This()) void {
        std.log.info("count is now {d}", .{self.count.get()});
    }
};
var w = Watcher{ .count = sig };
_ = try ctx.useEffect(&w, Watcher.run);

The context-pointer pattern (ctx_ptr + comptime f) is how Verve does closures without allocations: state travels in the struct, the function is comptime-known.

Owners

Every signal and effect registers with an Owner — the reactive scope. On the server each request gets an owner that is disposed when the response is sent; on the client the runtime's root owner lives for the page. Disposing an owner unsubscribes its effects from every signal they read — no leaks, no stale callbacks.

batch and untrack

// Coalesce writes — effects run once after the block, not per write:
verve.batch(&updater, Updater.run);

// Read without subscribing (logging, conditions):
const v = verve.untrack(i32, sig, readFn);

Live demo

The counter below is server-rendered with the real count, then hydrated by a WASM chunk. Each click writes one signal; the on_set hook mirrors the write into the single bound <span> — nothing else re-renders.

Verve Counter

0

Total clicks: 0

Server vs client

The same Signal/Effect types compile to both targets:

  • Server — signals seed SSR output (bind attributes carry the initial value into the HTML). The request owner disposes everything at end of render.
  • Client (WASM) — hydration walks the DOM, allocates a signal per z-bind attribute, and wires Signal.on_set to a DOM text setter. Island chunks register named signals via verve.registerI32("name", 0) and mutate them with verve.signalSetI32 — the runtime routes the write to the bound element.

Binding values in SSR

ctx.span().bind("count").textInt(initial)

renders <span z-bind="count">0</span>. After hydration, any chunk that calls signalSetI32("count", n) updates that span — and only that span.

Next: Rendering covers the node tree those bindings live in.