Skip to content

Batch, Dispose and Full Demo

Batch

By default, each .set() fires effects immediately. signal_batch(|| { ... }) defers all effects until the closure finishes, running them once with the final state. This avoids unnecessary intermediate renders in updates that affect multiple signals at once.

Two .set() calls inside the batch result in only one effect execution.

05-batch.zolo
Playground
// Feature: signal_batch — coalesce multiple writes into one effect run

// Syntax: `signal_batch(|| { ... multiple Signal.set calls ... })`.

// Effects depending on any of the changed signals run once at the

// end of the batch, not after each individual set.

// When to use: animation frames, bulk imports, transactions where

// intermediate states are noise.


use std::Signal

let x = signal(0)
let y = signal(0)
let runs = signal(0)

let e = effect(|| {
    print("x={x.get()} y={y.get()}")
    // `peek` reads without tracking, so this self-write does not

    // re-trigger the effect.

    runs.set(runs.peek() + 1)
})
// expected: x=0 y=0   (initial run)


// Without batch, this would print twice. With batch, it prints once.

signal_batch(|| {
    x.set(10)
    y.set(20)
})
// expected: x=10 y=20


print("effect runs: {runs.peek()}")
// expected: effect runs: 2   (initial + one batched)

Dispose

effect returns a handle. Calling handle.dispose() removes the effect from all subscribers and it never fires again. Use in unmounted components, cancellations, or to free memory in long-running apps.

After .dispose(), writes to the signal produce no output.

06-dispose.zolo
Playground
// Feature: effect.dispose() — unsubscribe an effect from all its signals

// Syntax: `let e = effect(|| { ... }); e.dispose()`.

// When to use: component unmount, cancellation, freeing memory in

// long-running apps. After dispose, the effect never fires again.


use std::Signal

let count = signal(0)

let e = effect(|| {
    print("count = {count.get()}")
})
// expected: count = 0


count.set(1)
// expected: count = 1


e.dispose()

// After dispose, sets do nothing visible — the effect is gone.

count.set(2)
count.set(3)
print("done")
// expected: done

Full Demo

With the three primitives mastered — signal, computed and effect — you can build expressive reactive pipelines. The example below composes everything together, including signal_batch for an atomic update:

Full pipeline: two computeds derived from a signal, unified effect and batch.

08-full-demo.zolo
Playground
// Feature: Putting it all together — signal + computed + effect

// A compact reactive pipeline showing how the three primitives

// compose. Read this file last; the others isolate each piece.


use std::Signal

let count = signal(0)
let doubled = computed(|| { count.get() * 2 })
let parity = computed(|| {
    if count.get() % 2 == 0 { return "even" }
    return "odd"
})

let e = effect(|| {
    print("count={count.get()} doubled={doubled.get()} ({parity.get()})")
})
// expected: count=0 doubled=0 (even)


// Single batched update — effect fires once at the end.

signal_batch(|| {
    count.set(3)
})
// expected: count=3 doubled=6 (odd)


count.set(10)
// expected: count=10 doubled=20 (even)

Challenge

Extend the full demo by adding a secondary effect that prints only when parity changes from "even" to "odd" (use an auxiliary signal to store the last value).

enespt-br