computed()
A derived value that stays live. The function re-evaluates whenever any state read inside it changes, and the result is itself a readable state:
const C = (props, { state, computed }) => {
const qty = state(2);
const total = computed(() => qty.value * (props.price?.value ?? 0));
return html`<b>${total}</b>`; // re-renders when qty OR props.price change
};
Write the sources, never the result. The derivation is disposed with the component; a dead component never computes again.
The problem
Two real bug classes motivated computed(). Both were found while
building the Studio catalog, not imagined.
The captured-once snapshot. A component receives a live prop and derives something from it during setup:
// BUG: reads the prop ONCE, at construction
const position = { top: props.top.value + 10, left: props.left.value };
Setup runs once, and that is the whole point of the architecture, so this
derivation is frozen forever. The Modal block shipped exactly this bug:
panels fed live anchor coordinates opened at 0,0 because the coordinates
were read before the caller measured them.
The hand-rolled pipeline. Without a derivation primitive, components
invent one: a version counter state, bumped from subscriptions, that
render bindings read to know when to recompute. The gantt block’s first
version did this, and hit a second trap inside it: version.value++
reads the state it writes, which inside a tracked context subscribed the
callback to itself (LJS-203: update loop detected).
Mechanics
computed() makes the live derivation the easy path:
// FIXED: a derived state that follows its sources
const position = computed(() => ({
top: props.top.value + 10,
left: props.left.value,
}));
// position.value is always current; ${position} in a template is live
Mechanics worth knowing:
- Eager and synchronous. The function runs immediately and on every source change; after a source assignment returns, the computed value is current. No scheduling model to reason about.
- Auto-tracked. Whatever states the function reads (local states,
props, stores) become its dependencies. Dependencies re-track on every
run, so conditional reads work (
a.value ? b.value : c.valuetracksbonly whileais true). - Invisible to the snapshot heuristic. The initial evaluation runs
during setup, but its reads are binding reads, not
template-construction reads, so they never count toward the dev
LJS-202snapshot warning, so usingcomputed()correctly cannot trip the very warning it exists to prevent. - Instance-scoped. The derivation is registered with the component and
disposed on unmount. The test suite pins this: after
unmount(), source changes no longer execute the function. - A state like any other. Render it (
${total}), pass it as a prop (it stays live across the boundary),subscribe()to it,peek()it.
Related primitive: subscribe(cb) runs callbacks untracked with
addEventListener semantics, pinned to its one source state. That engine
guarantee is what makes the gantt’s version.value++ safe today; the
component that exposed the loop now runs the exact same line, protected by
the engine instead of working around it.
In practice
Live panel anchoring (the bug that motivated the feature, fixed):
const Anchored = (props, { computed }) => {
const top = computed(() => (props.anchor?.value?.bottom ?? 0) + 1);
const left = computed(() => props.anchor?.value?.left ?? 0);
return html`<${Modal} position="absolute" top="${top}" left="${left}" header="${false}">
${props.children}
</${Modal}>`;
};
A filtered count beside a list, derived from the same array the list mutates in place:
const Inbox = (props, { state, computed }) => {
const items = state([{ unread: true }, { unread: false }]);
const unread = computed(() => items.value.filter((i) => i.unread).length);
const readAll = () => {
items.value.forEach((i) => (i.unread = false)); // mutate in place
items.touch(); // notify once
};
return html`<div>
<h3>Inbox <small>(${unread} unread)</small></h3>
<button onclick="${readAll}">Mark all read</button>
</div>`;
};
touch() notifies the computed, the computed notifies the heading: one
mutation pass, no clones, and the derived count can never be stale.
Reference
const C = (props, { computed }) => {
const derived = computed(fn); // State<ReturnType<fn>>
};
fnruns immediately and on every change of any state it reads.- The result is read-only by convention: write the sources.
- Disposed with the component instance.
- For module-scope derivations, derive at use sites from
store()values or wrap a component around the derivation;computedis a component tool by design; disposal needs an owner.