signal()#
import { signal } from 'janux';
const count = signal(0);The reactive primitive everything else is built on: a mutable cell that tracks who reads it. signal<T>(initial: T): Sig<T>.
| Member | Type | What it does |
|---|---|---|
value |
T (get/set) |
Reading inside a watch or computed subscribes to it. Writing notifies subscribers. |
peek() |
() => T |
Reads without subscribing — the targeted version of untrack. |
readers() |
() => number |
Live subscriber count. |
import { signal, watch } from 'janux';
const count = signal(0);
watch(() => console.log('count is', count.value)); // logs immediately: 0
count.value = 1; // logs: 1
count.peek(); // 1 — no subscription createdWrites are deduped by Object.is#
Assigning the same value notifies nobody, so an idempotent write is free:
const name = signal('ada');
name.value = 'ada'; // no notification at all
name.value = 'grace'; // notifiesObject.is semantics mean NaN is stable (NaN → NaN is not a change) while 0 and -0 are different. Objects compare by identity: mutating a nested field of a signal's object value does not notify — assign a new object, or model that state with schema inside a component, where every field is tracked for you.
You rarely need this directly#
Inside a component or store, state is already a fine-grained reactive object and derived is already a computed — both built on signals, both projected to agents. Reach for raw signals when you're writing framework-level code: a custom integration, a foreign-runtime bridge, or a helper that must be reactive outside any component.
Component state (preferred — schema-typed, agent-visible, auditable):
state: schema({ count: int() }),A raw signal (local, invisible to agents, yours to manage):
const scrollY = signal(0);readers() and ownership#
readers() exists so an owner can reclaim a signal nobody observes anymore. Subscriptions are removed automatically when the reading effect is disposed — see ownership.