createSignal
A minimal signal: [read, write], with optional broadcast over the shared subscribers bus so unrelated modules can react to a change.
API
createSignal(value, options?)
Parameters
| Parameter | Description | Type | Default |
|---|---|---|---|
value | Initial value | T | Required |
options.subscriber | Event name; broadcasts on subscribers when changed | string | undefined |
options.equals | How to decide "did it change" | boolean | ((prev: T, next: T) => boolean) | true |
equals semantics:
| Value | Behaviour |
|---|---|
omitted / true | Object.is — reference/value equality (standard signal semantics) |
false | Every write counts as a change and notifies |
| a function | Return true to mean "equal, skip the notification" |
Return
[getter, setter].
Example
js
import { createSignal, isEqual, subscribers } from 'ranuts';
const [count, setCount] = createSignal(0, { subscriber: 'count-changed' });
subscribers.tap('count-changed', () => render(count()));
setCount(1); // notifies
setCount(1); // same value — no notification
// Opt in to deep comparison when you actually need it
const [tree, setTree] = createSignal(initial, { equals: isEqual });Notes
- Reference equality by default. A freshly built but deep-equal object is a change. This matches standard signal semantics and keeps writes O(1).
- Deep comparison is opt-in via
{ equals: isEqual }— the cost is then visible at the call site. subscriberis optional. Without it the signal is purely local state.
Changed in 0.3
Two fixes that change behaviour:
{ equals: true }used to mean "always equal", freezing the signal so it never updated. It now means "use the default comparison", consistent withundefined.- Every write used to run
cloneDeep+isEqualon top ofequals. That put an O(data-size) copy on the write hot path, and the extra deep check overrodeequals, so{ equals: false }("always notify") silently did nothing for deep-equal values. Both are gone.