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 throwschroma: Show requires props.when to be a getter (function).fallback(optional) — function returning the node shown whenwhenis falsy. If omitted, nothing is shown in that state.- children — function returning the content shown when
whenis truthy. A non-function child is also accepted and mounted once (never toggled reactively on its own, only via the wrappingShow).
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 throwschroma: For requires props.each to be a getter (function).- children (required) —
(item) => noderender function. Missing it throwschroma: 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.