Skip to content

Signals Deep Dive

Core Design

Each signal is a SignalState<T> instance with a version counter and subscriber set:

ts
class SignalState<T> {
  private version = 0
  private subscribers = new Set<Effect>()

  get value(): T {
    track(this)
    return this._value
  }

  set value(newValue: T) {
    if (Object.is(this._value, newValue)) return
    this._value = newValue
    this.version++
    trigger(this)
  }
}

How Tracking Works

When a signal's .value is read inside an effect or computed, the signal registers the currently active effect as a subscriber. This creates a dependency graph that knows exactly which effects to notify when a signal changes.

Lazy Computed

computed values only recalculate when their .value is read. If nothing reads a computed, it never executes its derivation function — even if its dependencies change.

Batching Strategy

Multiple signal updates inside a batch() are coalesced into a single notification:

ts
batch(() => {
  a.value = 1
  b.value = 2
  c.value = 3
  // effects run once after batch exits
})

Why Signals?

Signals outperform Virtual DOM diffing by eliminating the need to diff subtrees whose data hasn't changed. Nexa combines signals with VDOM to get the best of both approaches.

Released under the MIT License.