Reactivity
kerf’s reactivity primitive is @preact/signals-core re-exported through src/reactive.ts. The whole API is four functions and two types.
2.1 signal(initialValue)
Section titled “2.1 signal(initialValue)”import { signal } from 'kerfjs';
const count = signal(0);count.value; // → 0 (read)count.value = 7; // (write — notifies subscribers)A signal is a single piece of reactive state. Reads via .value are tracked when they happen inside an effect() or computed(). Writes via .value = … trigger every effect that read this signal during its previous run.
2.2 computed(fn)
Section titled “2.2 computed(fn)”import { computed, signal } from 'kerfjs';
const a = signal(1);const b = signal(2);const sum = computed(() => a.value + b.value);
sum.value; // → 3a.value = 10;sum.value; // → 12A computed is a derived signal. Its body is re-run whenever any signal it reads changes. The result is cached until a dependency mutates.
computed is read via .value, just like signal. From a consumer’s perspective, you can’t tell whether a value is a raw signal or a computed — which is the point.
2.3 effect(fn)
Section titled “2.3 effect(fn)”import { effect, signal } from 'kerfjs';
const count = signal(0);const dispose = effect(() => { console.log('count is', count.value);});// → "count is 0" (synchronous initial run)
count.value = 1;// → "count is 1"
dispose();count.value = 2;// (nothing logged — effect is disposed)An effect() runs its body synchronously once on creation, then re-runs it whenever any signal read during the last run changes. Returns a disposer that tears the effect down.
mount() is built on effect() — same semantics, with kerf’s segment-aware diff as the side effect.
2.4 batch(fn)
Section titled “2.4 batch(fn)”import { batch, effect, signal } from 'kerfjs';
const a = signal(1);const b = signal(2);effect(() => console.log(a.value + b.value));// → "3"
batch(() => { a.value = 10; b.value = 20;});// → "30" (one log, not two)Coalesces multiple writes inside fn into a single re-run of any subscribed effect / computed. Useful when an action mutates several signals atomically and you don’t want consumers to see intermediate states.
2.5 The Signal<T> and ReadonlySignal<T> types
Section titled “2.5 The Signal<T> and ReadonlySignal<T> types”import type { ReadonlySignal, Signal } from 'kerfjs';
function reset(s: Signal<number>) { s.value = 0; // OK — Signal allows writes}
function display(s: ReadonlySignal<number>) { return s.value; // OK — read-only // s.value = 0; // type error — ReadonlySignal forbids writes}computed() returns ReadonlySignal<T>. signal() returns Signal<T>. Stores expose state: ReadonlySignal<TState> so consumers can’t bypass the action layer.
2.6 What signals are NOT
Section titled “2.6 What signals are NOT”- They are not deep-reactive. Mutating an array or object inside
signal.valuedoes NOT trigger subscribers. Always assign a new value:// wrong — silently doesn't notifycount.value.push(1);// rightcount.value = [...count.value, 1]; - They don’t track property accesses on plain objects — just
.valueon signal/computed instances. - They are not async. There’s no scheduling, no concurrent mode. Effects run synchronously when their dependencies write.
2.7 When to use raw signals vs. stores
Section titled “2.7 When to use raw signals vs. stores”- One consumer reads it = signal. Local UI state (this dialog’s open/closed, this counter’s value, this slider’s position) belongs in a signal scoped to the component that owns it.
- Two+ consumers / multi-step mutations / cross-route lifetime = store. See §3 Stores.