Targ Apps Docs
Chroma

Derived state

Compute values from state with useMemo chains and show them with reactive function children.

Chroma has no separate computed or watch API. Derived state is useMemo chains, and views are function children or function props. When a source signal changes, bindings re-run synchronously, so a read after a write is never stale.

Three layers

LayerPrimitiveRole
SourcehookState, useReducerInputs the user or a store writes to
DeriveduseMemoCached computation; returns a getter
ViewFunction child or function propRe-runs when its getters change
// src/components/ItemList.chr
import { hookState, useMemo, e, For } from "../../pkg/chroma.js";

export function ItemList() {
  const [query, setQuery] = hookState("");
  const [items] = hookState([
    { id: 1, label: "Rent" },
    { id: 2, label: "Groceries" },
  ]);

  const filtered = useMemo(() => {
    const q = query().trim().toLowerCase();
    if (!q) return items();
    return items().filter((item) => item.label.toLowerCase().includes(q));
  }, [query, items]);

  return e(
    "section",
    null,
    e("input", {
      value: query,
      onInput: (ev) => setQuery(ev.target.value),
      placeholder: "Search",
    }),
    e("ul", null, e(For, { each: () => filtered(), children: (item) => e("li", null, item.label) })),
  );
}

setQuery("gro") writes the query signal. The memo re-runs and writes a new array to its own signal. For is subscribed to filtered, so it rebuilds the list. The <input> is untouched.

Chained memos

List the upstream memo's getter in the deps array:

const [width, setWidth] = hookState(100);
const [height] = hookState(50);

const area = useMemo(() => width() * height(), [width, height]);
const label = useMemo(() => `Area: ${area()} px`, [area]);

return e("p", { "data-area": label }, label);

setWidth(200) re-runs area, which updates its signal, which re-runs label, which updates the attribute and text bindings. Only subscribers of changed signals run.

Getter or value

e("p", null, count)              // reactive
e("p", null, () => count() * 2)  // reactive, derived inline
e("p", null, doubled)            // reactive: a useMemo getter
e("p", null, doubled())          // frozen: evaluated once

Function props follow the same rule, for example class: () => "theme-" + theme(). The rule is explained in Reactivity.

Side effects on derived data

Use an effect when derived state should trigger imperative work, and pass getters as deps:

const rows = useMemo(() => buildRows(filtered(), page()), [filtered, page]);

useLayoutEffect(() => {
  animateRows(listRef.current);
}, [rows]);

Equivalents to other libraries

ElsewhereChroma
computed(() => ...)useMemo(() => ..., [deps]) or an inline () => ... child
watch(source, fn)useEffect(fn, [source])
effect(() => ...) (auto-track)useEffect(fn) with deps omitted

Show and derived lists

Show evaluates its children once. A derived list read inside Show children is therefore a snapshot, even though the memo keeps updating. Gate with Show and render the list with For and a getter each:

// src/components/FilteredList.chr
import { e, Show, For, useMemo } from "../../pkg/chroma.js";

export function FilteredList(props) {
  const visible = useMemo(
    () => props.todos().filter((t) => props.matches(t)),
    [props.todos],
  );

  return e(
    Show,
    { when: () => visible().length > 0, fallback: () => e("p", null, "Nothing to show") },
    () => e("ul", null, e(For, { each: () => visible(), children: (t) => e("li", null, t.text) })),
  );
}

The failing version and its explanation are in Pitfalls.

Next

On this page