derived
Read-only reactive binding. The RHS is an expression that recomputes whenever any dependency changes; the bound name is never directly written.
What it means
A derived cell is the third state shape (per SPEC §6.6): its value is a pure function of other reactive bindings. The compiler tracks the dependencies automatically; on any dep change, the cell re-evaluates and downstream consumers re-render.
Two non-negotiable rules:
-
Derived cells are write-forbidden.
Assigning into a derived cell fires
E-DERIVED-WRITE
(§6.6.8) at compile time. In-place mutation
(
@derivedArr.push(x)) fires the sibling error E-DERIVED-VALUE-MUTATE (§6.6.18). Mutate the upstream cell instead. -
Validators are forbidden on derived cells.
Predicates apply to inputs; derived outputs
don't have an independent validity surface. Adding
reqto a derived cell fires E-DERIVED-WITH-VALIDATORS .
Syntax
The
const <name> = expr
form is the SOLE declaration syntax for derived
reactive values (§6.6.1). The
@derived
keyword form is explicitly NOT recognised —
attempting it fires
E-REACTIVE-001
with a hint suggesting
const <name> = expr.
(The
derived=expr
attribute on
<engine>
is a separate construct — see the engine form
below.)
// Numeric derived
const <doubled> = @count * 2
// Markup-typed derived (§6.6.17)
const <badge> = <span class="badge">${@user.name}</span>
// String derived
const <greeting> = "Hello, " + @user.name
Derived engines (Tier 2 form):
<engine for=Phase derived=computePhase(@a, @b)>
<Idle/>
<Loading/>
<Ready/>
</engine>
Derived engines never carry
initial=
(fires
E-DERIVED-ENGINE-NO-INITIAL
)
and never accept writes — the variant is the
output of the derived expression.
Worked example
Live search. A search input drives a derived
@filtered
cell; the
${@filtered}
list re-renders automatically on every keystroke. No
manual subscribe, no useEffect, no
computed()
wrapper.
<program>
<contacts> = [
{ name: "Alice", email: "alice@example.com" },
{ name: "Bob", email: "bob@example.com" },
{ name: "Charlie", email: "charlie@example.com" },
]
<query> = ""
const <filtered> = @contacts.filter(c => c.name.toLowerCase().includes(@query.toLowerCase()))
<input bind:value=@query placeholder="Search by name">
<ul>
${ for (let c of @filtered) { lift <li>${c.name} — ${c.email}</li> } }
</ul>
</program>
Notice: the
@filtered
cell never gets a write site. It's computed from
@contacts
and
@query;
both are tracked automatically. If you tried
@filtered = [...]
anywhere, the compiler would reject it at compile time
(E-DERIVED-WRITE).
Semantics
- Lazy evaluation by default. The derived expression evaluates on first read. If nothing reads it, it doesn't run.
-
Dep tracking is static, not runtime.
Every
@xread appearing inside the derived RHS becomes a compile-time dependency — regardless of conditional reachability. Both branches of anifcontribute their reads to the dep set. Runtime auto-tracking (SolidJS-style branch-conditional subscription) is explicitly rejected by §6.6.3 forconst <name> = exprcells outside^{}meta blocks. - Result caching. The computed value is cached. Consumers re-read the cached value without re-running the expression until a dep changes.
- Markup-typed derived cells are first-class. A derived expression whose RHS produces markup (per §6.6.17) is a markup-typed value — interpolatable, slot-fillable, return-valuable. The same way a string-typed derived cell is a string.
-
Reactivity attributes are forbidden.
debounced=/throttled=on a derived cell isE-DEBOUNCED-WITH-DERIVED. Wrap the upstream input instead.
Edge cases
Don't mutate the value, even if the type is mutable
@derivedArr.push(x)
on a const-derived array is
E-DERIVED-VALUE-MUTATE.
Even if the mutation would technically work
(JS arrays are mutable), the next dep-fire will
recompute the value and clobber the push. Mutate the
upstream cell:
@items = [...@items, x].
Self-reference is a cycle
A derived cell that reads itself in its own RHS is a structural cycle. The compiler detects this at the dep-graph pass and rejects.
Don't put side effects in the RHS
Derived RHS expressions should be pure functions of
their deps. Writing to other cells inside the RHS, doing
IO, calling
fetch()
— all of these break the "this value IS this
function of these inputs" contract. Side effects belong
in event handlers or
<onTransition>
elements, not derived RHS.
Related
- E-DERIVED-WRITE — fires on direct reassignment of a derived cell (§6.6.8).
-
E-DERIVED-VALUE-MUTATE
— fires on in-place mutation
(
@derived.push(…); §6.6.18). - E-DERIVED-WITH-VALIDATORS — fires when validator predicates are placed on a derived cell.
-
E-DERIVED-ENGINE-NO-INITIAL
— derived engines forbid
initial=. - <engine> — derived engines are an engine form.
Specification
Normative text: SPEC §6.6 (Shape 3 — derived) + §6.6.17 (markup-typed derived) + §6.6.18 (derived value-mutate forbidden) + §51 (derived engines). Spec lives at compiler/SPEC.md .