Reactivity in Depth
Pyreon's reactivity is fine-grained: a signal change updates exactly the DOM that depends on it, never a component re-render. The mental model is one rule:
Components run once. What's reactive depends on WHERE you read a signal.
Master that rule and every other behavior follows.
The three primitives
// @check
import { signal, computed, effect } from '@pyreon/reactivity'
const count = signal(0) // read: count(), write: count.set(1)
const double = computed(() => count() * 2) // derived, cached, auto-tracked
effect(() => console.log('count is', count())) // re-runs when count changessignal<T>(initial)— a callable.count()reads (and subscribes the current scope),count.set(v)/count.update(fn)write.computed(fn)— a memoized derived signal; recomputes only when a dependency changes.effect(fn)— a side-effect that re-runs when its tracked dependencies change. Return a cleanup function or useonCleanup.
Signals, computeds, and effects, live:
Derived values with computed:
Side effects on change:
Where reactivity lives
Because components run once, a signal is reactive only where it's read inside a tracking scope:
function Counter() {
const count = signal(0)
// Reactive — JSX text expression is a tracking scope:
return <button onClick={() => count.set(count() + 1)}>{() => count()}</button>
}DOM text / attributes with signal reads → reactive (the compiler wraps them).
Component props containing signal reads → reactive (the compiler wraps with
_rp).const x = props.yin JSX → reactive (the compiler inlinesprops.yback at the use site).Destructuring props /
letfrom props → captured once, static.
Batching and untracking
batch(fn)— coalesce multiple writes into one update pass. Three-plus writes in a row should be batched.untrack(fn)— read signals without subscribing, when you want a value but not a dependency.createSelector(source)— O(1) keyed membership for large lists (e.g. selected-row highlighting) instead of O(n) per-row checks.
import { batch, untrack } from '@pyreon/reactivity'
batch(() => {
first.set('Ada')
last.set('Lovelace')
}) // one update, not twoCommon pitfalls
signal(5)to write. That reads and ignores the argument. Usesignal.set(5)/signal.update(n => n + 1). (Dev mode warns.)Conditional reads hide tracking.
{() => cond() ? a() : ''}only subscribes toawhilecondis true; a write toain the same batch that flipscondis missed. Read both into consts first.Destructuring props.
const { value } = propscaptures once. Readprops.valuein the reactive scope, or usesplitProps..peek()inside an effect/computed. It bypasses tracking — only use it deliberately for loop-prevention or imperative-ref reads.