Targ Apps Docs
Chroma

Reactivity

The one rule of Chroma's reactivity, and how signals, bindings, and dependency tracking behind it work.

Chroma's reactivity is fine-grained. Instead of re-rendering components and diffing trees, individual bindings subscribe to individual signals and re-run when those signals change.

The one rule

A child or an attribute is reactive only if you pass it as a function.

e("p", null, count)              // reactive: updates when count changes
e("p", null, count())            // static: evaluated once, never updates
e("p", null, () => count() * 2)  // reactive derived expression

The same applies to attributes:

e("main", { class: () => theme() })  // reactive attribute
e("main", { class: theme() })        // static, evaluated once

Components run once, so there is no re-render that would re-evaluate an expression. The getter itself is the subscription. Every other page in these docs links here for this rule.

Signals, bindings, and the graph

A signal is a mutable value slot. hookState, useReducer, and each useMemo allocate one. You only ever hold a getter and a setter.

A binding is a function plus a target describing what to do with its result:

TargetCreated byOn evaluation
AttributeFunction prop on an elementSets or removes the attribute
Dynamic childFunction child of an elementReplaces the slot's content (text or nodes)
EffectuseEffect, useLayoutEffect, Show, ForRuns the side effect and stores the returned cleanup
MemouseMemoWrites the result into the memo's signal

The dependency graph records which bindings are subscribed to which signals.

The tracking cycle

  1. Evaluation opens a tracking scope. Before a binding runs, its previous dependencies are cleared, because they can change between runs.
  2. Reads register. Every getter call inside the function subscribes the innermost running binding to that signal. Duplicate reads subscribe once.
  3. The scope closes and the result is applied to the target.
  4. Writes notify. A setter compares with Object.is. If the value changed, it stores it and synchronously re-runs every subscribed binding.

Dependencies are rebuilt on every run, so a binding that stops reading a signal unsubscribes from it automatically.

Untracked execution

Some code reads signals without subscribing:

  • Effect and memo bodies when an explicit deps array is given (the array defines the subscriptions).
  • Cleanup functions.
  • For's item render function.
  • Show's initial evaluation of fallback and children.

Effect timing

  • useLayoutEffect runs synchronously after render appends the tree, before paint. Effects queued by other effects also run.
  • useEffect runs its first time in a microtask after mounting.
  • Re-runs of both kinds are synchronous with the signal write that triggered them.

Change detection

Change detection is Object.is on the signal's value. Writing the same primitive is a no-op. Mutating an object or array in place does not notify, because the reference is the same. Write a new reference.

setTodos([...todos(), newTodo]);   // new array: notifies
todos().push(newTodo);             // same reference: silent

Errors in reactive code

Errors thrown inside effect and memo bodies, effect cleanups, reducers, binding expressions, and Provider children are caught and logged to the console with a chroma: prefix. The app keeps running and the failed run produces no update. Misuse of e, render, Show, and For (for example a missing required prop) throws an error instead. Exact messages: API reference.

Next

On this page