Reactivity
Dependency tracking: derivations discover what they read, so a write re-runs exactly the work that depends on it — and nothing else.
New in Domma. M.computed() and M.effect() track their own dependencies at runtime. See the Models showcase for the underlying model API.

Tracked Computeds

A computed is lazy: its body does not run until something reads it, and the cached value is reused until a field it actually read changes. The counters below show how often each body runs.

M.computed() .get() .peek() .dispose()
Total
30
Body evaluations
1

Change Price or Quantity and the counter rises. Type in Note — the model updates, but the computed never re-runs. Press Read total again to confirm repeated reads are served from cache.

const order = M.create(
    { price: {}, qty: {}, note: {} },
    { price: 10, qty: 3, note: '' }
);

const total = M.computed(() => order.get('price') * order.get('qty'));

total.get();              // 30 — body runs now
total.get();              // 30 — cached, body does not run

order.set('note', 'hi');  // 'note' was never read → no re-evaluation
total.get();              // 30 — still cached

order.set('qty', 4);
total.get();              // 40 — a dependency moved, so it re-evaluates

Effects

An effect runs immediately to collect its dependencies, then again whenever any of them change. It replaces hand-wiring a subscription and comparing field names by hand.

M.effect() M.untracked() M.flush()

Changing Session id does not re-run the effect — it is read inside M.untracked().

const stop = M.effect(() => {
    const user  = prefs.get('username');   // tracked
    const theme = prefs.get('theme');      // tracked

    // Read without subscribing to it
    const session = M.untracked(() => prefs.get('sessionId'));

    render(user, theme, session);
});

stop();   // unsubscribe

Batching

Writes never recompute anything synchronously. They mark dependents dirty and schedule a single microtask flush, so a burst of writes collapses into one pass. Use M.flush() when you need a derived value to settle immediately.

Effect runs
1

Either button adds exactly one run: three writes in the same tick share one flush.

// Three separate writes, same tick → ONE effect run
form.set('a', 1);
form.set('b', 2);
form.set('c', 3);

// One batched write → also ONE effect run
form.set({ a: 1, b: 2, c: 3 });

// Need it settled right now (tests, or reading straight after a write)?
form.set('a', 9);
M.flush();

Conditional Dependencies

Dependencies are re-collected on every run, so a derivation stops listening to the branch it no longer takes. Switch the mode below and watch which input still triggers a re-run.

In Read X mode, editing Y does nothing. Switch modes and the roles swap.

M.effect(() => {
    // Only ONE of x / y is a dependency at any moment
    const value = state.get('mode') === 'x'
        ? state.get('x')
        : state.get('y');

    log(value);
});

Components

Domma.component() uses tracking automatically — the definition syntax is unchanged. Computeds re-evaluate only when a field they read changes, and a burst of writes produces one render.

Touch unrelated field writes to the model but triggers neither a computed re-evaluation nor a render.

Domma.component('order-summary', {
    template: '<p>{{label}}</p>{{#if freeDelivery}}<em>Free delivery</em>{{/if}}',

    data() { return { items: 1, unitPrice: 25, discount: false, note: '' }; },

    computed: {
        // Re-evaluates only when items / unitPrice / discount change
        label() {
            const gross = this.data.items * this.data.unitPrice;
            return `${this.data.items} item(s) — £${this.data.discount ? gross * 0.9 : gross}`;
        },
        freeDelivery() { return this.data.items * this.data.unitPrice > 50; }
    }
});
Computeds must be synchronous. Dependency collection ends at the first await, so anything read after it is invisible to the graph. Fetch first, then write the result to the model.

Method Reference

Method Returns Description
M.computed(fn, options?) ComputedRef Lazily-evaluated derived value. Caches until a tracked dependency changes.
M.effect(fn, options?) Function Runs now, and again on dependency change. Returns a stop function.
M.untracked(fn) any Read values without registering them as dependencies.
M.flush() void Settle pending reactive work immediately rather than on the microtask.
model.tracked() Proxy Read-tracked, write-through view. Writes still validate, notify and persist.
ref.get() any Current value; registers a dependency on the caller.
ref.peek() any Current value without registering a dependency.
ref.dispose() void Unlink from the dependency graph.
Full guide: rules, limits and compatibility notes are in docs/Reactivity.md.