Targ Apps Docs
Chroma

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

SymptomLikely causeFix
dependencies must be getters in console[count()] or a primitive in depsPass [count]
update loop in consoleSetter inside an effect or memo that lists the same signalNarrow deps, guard the write
ReferenceError in a memoA const declared below itReorder declarations
Crash on .valid or .errorsUpstream memo returned undefinedOptional chaining and defaults
Memos update, list rows do notList read inside Show childrenFor with each: () => derived()
List does not update after pushIn-place mutation, same referenceWrite a new array

Next

On this page