pyreon

Signal reads and writes

The pattern

Pyreon signals are callable functions. The API is tiny:

import { signal } from '@pyreon/reactivity'

const count = signal(0)

count()                          // read: 0
count.set(5)                     // write: 5
count.update((n) => n + 1)       // write via function: 6
count.peek()                     // read WITHOUT subscribing (rare — loop-prevention only)

In JSX, bare signal references auto-call (the compiler rewrites them):

const AutoCall = () => <div>{count}</div>               // compiled → <div>{() => count()}</div>
const AlreadyCalled = () => <div>count = {count()}</div> // already called — compiler leaves it alone

For reactive expressions, call the signal explicitly inside the expression:

const HotCold = () => <div class={() => (count() > 10 ? 'hot' : 'cold')}>{count()}</div>

Why

A callable signal is one identifier with two behaviours — read (no arg) or update (.set / .update / .peek). The alternative — .value getter/setter (Vue refs) — requires destructuring or property access that breaks reactivity tracking: const { value } = mySignal captures once. Pyreon chose callable for that reason.

Anti-pattern

const count = signal(0)

count(5)              // DOES NOT WRITE. The argument is read and ignored.
                      // Dev mode prints a warning; production silently no-ops.

count.value = 5       // TypeError — signals have no .value property
// BROKEN — destructuring loses reactivity
function Counter(props: { n: Signal<number> }) {
  const { n } = props            // captures the signal reference once, fine so far…
  const value = n()              // …but value is a plain number, not reactive
  return <div>{value}</div>      // never updates
}
// BROKEN — reading inside setup captures the initial value
const Counter = (props) => {
  const initial = props.count()  // static
  return <div>{initial}</div>    // never updates
}

// FIX — read inside the reactive expression
const Counter = (props) => {
  return <div>{() => props.count()}</div>
}
  • Detector: no static check for signal(value) writes — this is scope-tracked and requires type info (see future migrate_pyreon work)

  • Dev warning: signal.set() must be used to write — the runtime warns in dev mode on signal(value) calls that look like writes

  • Anti-pattern: "signal(newValue) to write" in context category

Signal reads and writes