Pitfalls
Common Chroma mistakes - dependency arrays, update loops, declaration order, Show with lists, and unguarded memos - with fixes.
These are the mistakes that most often cause stale UI or console errors. Chroma reports several of them in the browser console with a chroma: prefix; treat those messages as guardrails.
1. Dependencies must be getters
Dependency arrays list signal getters, never values.
// Wrong
useEffect(() => { loadChart(); }, [monthCount(), "transactions"]);
useMemo(() => rows(), [count()]);The console logs: chroma: dependencies must be getters (functions) - pass 'count', not 'count()'.
// Right
useEffect(() => { loadChart(); }, [monthCount, transactions]);
useMemo(() => rows(), [count]);See Hooks.
2. Update loops
Do not write to a signal inside an effect or memo that depends on that same signal. A write during evaluation re-triggers the binding. Chroma detects it, skips the re-entrant run, and logs chroma: update loop — binding #N wrote to a signal it depends on while still evaluating.
// Wrong: reads accountId, then writes it on every run
useEffect(() => {
setAccountId(normalize(accountId()));
}, [accountId]);Guard the write with the last value you synced, kept in a ref, and depend only on the trigger:
// src/screens/AccountSync.chr
import { e, hookState, useEffect, useRef } from "../../pkg/chroma.js";
export function AccountSync(props) {
const [accountId, setAccountId] = hookState(props.initialId);
const lastSynced = useRef(null);
useEffect(() => {
const id = accountId();
if (id !== lastSynced.current) {
lastSynced.current = id;
props.onAccountChange(id);
}
}, [accountId]);
return e("button", { onClick: () => setAccountId("acc-2") }, "Switch account");
}Use [] for one-shot setup and compare before writing inside reactive bodies. Never pass a useCallback result as a dependency, because dependencies are called as getters and that runs the callback.
3. Declaration order
Components run top to bottom once, so JavaScript's temporal dead zone applies. A memo must not reference a const declared below it.
// Wrong
const formData = useMemo(() => ({ id: resolvedId() }), [resolvedId]);
const resolvedId = useMemo(() => accountId() ?? defaultId(), [accountId, defaultId]);// Right: sources first, derived values after their inputs
const resolvedId = useMemo(() => accountId() ?? defaultId(), [accountId, defaultId]);
const formData = useMemo(() => ({ id: resolvedId() }), [resolvedId]);4. Show freezes lists: use For for derived data
Show evaluates its children function once, at mount, and only toggles visibility afterward. A list read inside those children is a snapshot. Memos and effects keep updating; the visible rows do not.
// Wrong: groups is read once when Show mounts its child
e(Show, { when: hasItems }, () =>
e("ul", null, listState().groups.map((g) => e("li", null, g.label))),
);// src/screens/GroupList.chr
import { e, Show, For, hookState, useMemo } from "../../pkg/chroma.js";
export function GroupList() {
const [items, setItems] = hookState([{ group: "Fruit" }]);
const addGroup = () => setItems([...items(), { group: "Veg" }]);
const listState = useMemo(() => ({ groups: items().map((i) => ({ label: i.group })) }), [items]);
const hasItems = () => listState().groups.length > 0;
// Show gates, For renders the list with a getter
return e(
"section",
null,
e("button", { onClick: addGroup }, "Add group"),
e(Show, { when: hasItems }, () =>
e("ul", null, e(For, {
each: () => listState().groups,
children: (g) => e("li", null, g.label),
})),
),
);
}This is the only place that explains this mistake; other pages link here. Details of both components: Control flow.
5. Guard derived memos
If a memo throws, the error is logged and the memo's value becomes undefined. Downstream memos that read a property crash in turn.
// Wrong
const validation = useMemo(() => validate(formData()), [formData]);
const canSubmit = useMemo(() => validation().valid, [validation]);// Right
const canSubmit = useMemo(() => validation()?.valid ?? false, [validation]);Quick reference
| Symptom | Likely cause | Fix |
|---|---|---|
dependencies must be getters in console | [count()] or a primitive in deps | Pass [count] |
update loop in console | Setter inside an effect or memo that lists the same signal | Narrow deps, guard the write |
ReferenceError in a memo | A const declared below it | Reorder declarations |
Crash on .valid or .errors | Upstream memo returned undefined | Optional chaining and defaults |
| Memos update, list rows do not | List read inside Show children | For with each: () => derived() |
List does not update after push | In-place mutation, same reference | Write a new array |