Targ Apps Docs
Chroma

Control Flow

Show and For — Chroma's reactive conditional and list components.

Chroma has no compiler, so it cannot make if/else or .map() lazy on its own — plain JS would evaluate every branch and every item eagerly, outside of any reactive scope. Show and For exist to give conditionals and lists the same "children as functions" treatment as everything else in Chroma: they mount their content once (preserving inner hooks) and drive updates through an internal Effect binding.

Show

Conditional rendering. Mounts both branches exactly once (preserving their internal hooks) and toggles visibility based on when:

e(Show, {
    when: loggedIn,                      // getter
    fallback: () => e("p", null, "Log in"),
}, () => e(Dashboard))
  • when (required) — getter (function) evaluated reactively. Missing it throws chroma: Show requires props.when to be a getter (function).
  • fallback (optional) — function returning the node shown when when is falsy. If omitted, nothing is shown in that state.
  • children — function returning the content shown when when is truthy. A non-function child is also accepted and mounted once (never toggled reactively on its own, only via the wrapping Show).

Truthiness follows JS rules (js_truthy in Rust): null, undefined, false, 0, and "" are falsy — everything else, including other falsy-looking values like NaN or empty arrays/objects, is truthy in this implementation.

Internally, Show renders both the fallback and the children branch — each evaluated once, under untracked — into two sibling <div> slots inside a <div style="display: contents"> wrapper, then flips each slot's display between contents and none whenever when() changes inside a reactive Effect binding. The branches themselves are never re-created or re-mounted.

For

Reactive list. Rebuilds child nodes only when the array read by each changes:

e(For, {
    each: todos,                          // getter returning an array
    children: (item) => e("li", null, item.text),
})
  • each (required) — getter returning an array. Missing it throws chroma: For requires props.each to be a getter (function).
  • children (required)(item) => node render function. Missing it throws chroma: For requires props.children to be a function: (item) => node.

Rebuilding is wholesale (no keyed diffing) — every change to the tracked array clears and recreates all list nodes, not just the changed ones. There is no key prop.

The item render function itself runs under untracked, so reading signals inside an item doesn't add extra subscriptions to the list binding — only the each getter is tracked. If the value read by each isn't an array, For renders nothing rather than erroring.

const [todos, setTodos] = hookState([{ id: 1, text: "Write docs" }]);

// ✅ new array reference → For's binding re-runs and rebuilds the list
setTodos([...todos(), { id: 2, text: "Ship it" }]);

// ❌ same array reference → no notification, list stays stale
todos().push({ id: 3, text: "Oops" });

See Reactivity Model for why reference identity matters here, and Limitations for the rationale behind not implementing keyed reconciliation yet.

On this page