Derived values
Read-only values computed from other actor reads, evaluated at resolution time.
class Timer extends LayoutActor {
elapsed = signal(0);
remaining = derived(() => Math.max(0, 30 - this.elapsed()));
progress = derived(() => this.elapsed() / 30);
label = derived(() => `00:${Math.ceil(this.remaining()).toString().padStart(2, "0")}`);
}A derived has no setter. It runs at resolution time, reads whatever it reads, and
returns a value for that frame.
One signal is animated. remaining reads elapsed, label reads remaining, and the
whole chain is recomputed from scratch on every frame — including when you scrub
backwards. Nothing is cached between frames, so there is no stale value to invalidate.
Use it for
- Values that should never be written directly.
- Presentation computed from state — geometry, transforms, formatted labels — so the view reads finished numbers instead of doing arithmetic.
- Convenience reads built from signals, channels or other derived values.
Derived chains
Derived values compose, and composing them is how presentation stays out of the renderer:
public dockMetrics = derived((): DockMetrics => {
const { width, height } = this.localLayout();
return resolveDockMetrics(this.skin(), this.dock.items().length, width, height);
});
public dockIconCenters = derived(() => {
const { iconSize, gap, paddingX, left, centerY } = this.dockMetrics();
// …one screen-space center per dock item
});
public windowVisuals = derived((): WindowVisual[] => {
const centers = this.dockIconCenters();
return this.windows().map((window, index) => ({
id: window.id,
transform: buildWindowTransform(window, centers[window.appId] ?? null),
opacity: clamp01(window.openProgress),
zIndex: 100 + index,
}));
});The renderer then does nothing but windowVisuals().map(...). See the
Desktop example for this pattern in full.
Authoring forms
Arrow form is the default. The function form with an explicit this type is equivalent —
both resolve this to the live per-frame actor.
// default
width = derived(() => this.scale() * 320);
// explicit this
width = derived(function (this: Card) {
return this.scale() * 320;
});Purity
A derived body runs during frame resolution, which means it is bound by the
determinism contract. No
Date.now(), no Math.random(), no reading or writing module-level mutable state. A
derived value that is not a pure function of its reads breaks seeking, and it breaks it
quietly — forward playback looks correct and only scrubbing backwards disagrees.
Do not use it for
derived reads channels; channels cannot read derived
Computing from a host-published value is exactly what derived is for —
derived(() => this.progress() * this.availableWidth()). The reverse does not exist:
channels are published from outside and cannot drive compile-time watches.