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.

frame 0·0.00selapsed 0.00 → remaining 30.00 → progress 0.000 → label 00:30

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

Instead ofUse
Writable statesignal
Host-fed runtime factschannel
Side effects on state changewatch

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.

On this page