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.value tracks b only while a is 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-202 snapshot warning, so using computed() 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>>
};
  • fn runs 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; computed is a component tool by design; disposal needs an owner.