Reactivity
Writable state, computed reads, host-published values and coordination are separate primitives. Picking the right one is most of actor design.
The runtime deliberately refuses to give you one general-purpose reactive box. Each primitive encodes a different claim about where a value comes from and who is allowed to change it — which is what keeps frame resolution pure.
Choosing a primitive
Signals
One flat writable value. The default.
Reactive signals
One nested state bag whose leaves animate independently.
Derived values
Read-only, computed from other reads.
Channels
A value the host publishes, not the timeline.
Groups
Namespaces related fields into one surface.
Overlay and offset signals
Layers transient motion over a base value.
Watch
Actor-local reactive response.
Events and subscribe
Typed coordination between actors.
The decision, in one pass
Ask these in order. The first yes is your answer.
| Question | Primitive |
|---|---|
| Is the value computed from other reads and never set directly? | derived |
| Is the value observed rather than authored — measured, fetched, simulated, sensed? | channel |
| Is it a nested bag whose individual leaves need to animate on their own? | reactiveSignal |
| Is it transient motion sitting on top of a canonical value? | overlaySignal |
| Anything else the timeline writes | signal |
Two of these are not state at all. group is namespacing, and
events are coordination — reach for them when
the question is "how do I organize this" or "how does another actor find out", not "where
does this value live".
The signal / channel line
If the value's history is authored — set, tween, play — it is a signal. If the
value is observed — measured, fetched, simulated, sensed — it is a channel. This is
the one distinction worth memorizing; almost every determinism bug is a channel value
that was modelled as a signal.
Ownership
Owners mutate their own canonical state. Non-owners emit semantic intents and let the owner decide.
// Wrong — reaching through another actor to write its state.
playButton.onPress(() => video.playback.currentTime.set(0));
// Right — emit intent, the owner responds.
subscribe(video, playButton, {
*pressed() {
yield* this.restart();
},
});That rule is what lets an actor be reused: it keeps every write to a piece of state inside the class that declared it, so its invariants hold no matter who is driving.