G · 03 guides

Guides / Reactivity

Reactivity

@aihu/signals provides the reactive foundation for the entire aihu framework. It uses a push-based, synchronous execution model: when a signal is written, all dependent effects run immediately.

signal<T>(initialValue)

Creates a writable reactive cell:

governedtypescripttypescript148 B
import { signal } from '@aihu/signals'

const count = signal(0)

count()       // read: returns 0
count(1)      // write: sets to 1, flushes effects

Signals are the atomic unit of state. They are not wrapped in objects or proxies — call them as functions to read or write.

computed<T>(fn)

Derives a read-only signal from other signals:

governedtypescripttypescript158 B
import { signal, computed } from '@aihu/signals'

const count = signal(0)
const doubled = computed(() => count() * 2)

doubled() // 0
count(5)
doubled() // 10

Computed signals are lazily evaluated and memoized. They re-evaluate only when a tracked dependency changes.

effect(fn)

Runs a side effect whenever tracked signals change. Returns a dispose function:

governedtypescripttypescript234 B
import { signal, effect } from '@aihu/signals'

const name = signal('world')
const dispose = effect(() => {
  console.log('Hello,', name())
})
// Logs: "Hello, world"

name('aihu')
// Logs: "Hello, aihu"

dispose() // stops the effect

Effects run synchronously after each signal write. They auto-track all signals read during their execution.

To clean up resources when an effect re-runs, return a cleanup function:

governedtypescripttypescript152 B
const dispose = effect(() => {
  const id = setInterval(() => tick(), 1000)
  return () => clearInterval(id)  // called before next run or on dispose
})

batch(fn)

Defers effect flushes until the batch function returns:

governedtypescripttypescript237 B
import { signal, effect, batch } from '@aihu/signals'

const a = signal(0)
const b = signal(0)

effect(() => console.log(a(), b()))

batch(() => {
  a(1)  // no flush yet
  b(2)  // no flush yet
})
// Now effects flush once with a=1, b=2

batch is useful when updating multiple signals that drive the same derived computation — it prevents intermediate renders. This is the preferred pattern for atomic multi-signal updates.

untrack(fn)

Reads signals inside fn without subscribing to them. Re-entrancy safe:

governedtypescripttypescript241 B
import { signal, effect, untrack } from '@aihu/signals'

const count = signal(0)
const multiplier = signal(2)

effect(() => {
  // Only subscribes to count, not multiplier
  const m = untrack(() => multiplier())
  console.log(count() * m)
})

untrack is re-entrancy safe — calling it from inside an effect or another untrack works correctly.

Reactivity inside @state

@state blocks don't reach for the raw @aihu/signals primitives directly — they use the wrapper intrinsics built on top of them: state() for private working values, prop() for the component's public host-settable surface, derived() for memoized computed values, and action() for named, agent-invocable operations. Every declared name is just a plain local binding in scope for the rest of the block and the template — there is no separate accessor bag to qualify through; you read and write count, not state.count.

Reactive patterns

Computed chains

Computeds can depend on other computeds:

governedtypescripttypescript115 B
const base = signal(10)
const doubled = computed(() => base() * 2)
const quadrupled = computed(() => doubled() * 2)

Only base is a writable signal; the derived chain updates automatically. Each computed is memoized — intermediate computeds don't re-run unless their own dependencies change.

Effects with cleanup

Return a cleanup function from effect to dispose resources before the next run:

governedtypescripttypescript217 B
const url = signal('/api/data')

effect(() => {
  const controller = new AbortController()
  fetch(url(), { signal: controller.signal })
    .then(r => r.json())
    .then(setData)
  return () => controller.abort()
})

The cleanup runs when url changes (before the next fetch starts) and when the effect is disposed.

The batch pattern for atomic updates

When multiple signals feed a single derived value, use batch to prevent intermediate states:

governedtypescripttypescript308 B
const hue = signal(215)
const saturation = signal(70)
const lightness = signal(55)

const color = computed(() =>
  `hsl(${hue()} ${saturation()}% ${lightness()}%)`
)

// Without batch: color re-computes 3 times
// With batch: color re-computes once
batch(() => {
  hue(0)
  saturation(100)
  lightness(50)
})

Push-based semantics

aihu signals are push-based: effects run synchronously after each signal write (or after a batch completes). There is no scheduler, no microtask queue, and no async rendering pipeline. This makes behavior predictable and side effects easy to reason about.