Reactivity API
Core reactive primitives for state management.
signal
Create reactive state. Primitives are wrapped in { value }, objects become reactive proxies.
function signal<T extends Primitive>(target: T): PrimitiveSignal<T>;
function signal<T extends object>(target: T): Signal<T>;
Parameters
| Name | Type | Description |
|---|---|---|
target | T | Initial value (primitive or object) |
Returns
- Primitives:
{ value: T }wrapper with reactivevalueproperty - Objects: Reactive proxy with direct property access
Examples
import { signal } from 'sigx';
// Primitives - use .value
const count = signal(0);
count.value++; // 1
const name = signal('World');
name.value = 'SignalX';
// Objects - direct access
const user = signal({ name: 'John', age: 30 });
user.name = 'Jane'; // Reactive
user.age++; // Reactive
// Arrays
const items = signal([1, 2, 3]);
items.push(4); // Reactive
items[0] = 10; // Reactive
// Collections
const map = signal(new Map());
map.set('key', 'value'); // Reactive
// Replace entire object
user.$set({ name: 'Bob', age: 25 });
Key-set tracking
Enumerating an object proxy inside an effect or computed — Object.keys(),
for…in, spread, rest destructuring — also subscribes to its key set, so adding
or deleting a key re-runs the reader. 'x' in obj subscribes to the same
dependency obj.x does. Arrays carry that dependency on length; Maps and Sets
track iteration and .size. See Enumerating Reactive
Objects.
effect
Run a function when its reactive dependencies change.
function effect(fn: EffectFn): EffectRunner;
Parameters
| Name | Type | Description |
|---|---|---|
fn | () => void | Function to run reactively |
Returns
EffectRunner - Callable function with a stop() method.
Called directly in a component's setup body, the effect is tied to that component and stops on unmount — the handle is there if you want it, not something you must keep. Created anywhere else (including inside onMounted or an async callback), stopping it is yours to do. See Effect cleanup.
Examples
import { signal, effect } from 'sigx';
const state = signal({ count: 0 });
// Effect runs immediately and on changes
const runner = effect(() => {
console.log('Count:', state.count);
});
state.count = 5; // Logs: "Count: 5"
// Stop the effect
runner.stop();
state.count = 10; // No log
computed
Create a lazily-evaluated derived value.
function computed<T>(getter: () => T): Computed<T>;
function computed<T>(options: { get: () => T; set: (v: T) => void }): WritableComputed<T>;
Parameters
| Name | Type | Description |
|---|---|---|
getter | () => T | Function that computes the value |
options.get | () => T | Getter for writable computed |
options.set | (v: T) => void | Setter for writable computed |
Returns
- Read-only:
Computed<T>- Call to get value:computed() - Writable:
WritableComputed<T>- Hasset()method
Examples
import { signal, computed } from 'sigx';
const state = signal({ count: 0 });
// Read-only computed
const doubled = computed(() => state.count * 2);
console.log(doubled()); // 0
state.count = 5;
console.log(doubled()); // 10
// Writable computed
const celsius = signal({ value: 0 });
const fahrenheit = computed({
get: () => celsius.value * 9/5 + 32,
set: (f) => celsius.value = (f - 32) * 5/9
});
fahrenheit.set(212);
console.log(celsius.value); // 100
watch
Watch reactive sources with a callback.
function watch<T>(
source: WatchSource<T>,
callback: WatchCallback<T>,
options?: WatchOptions
): WatchHandle;
Parameters
| Name | Type | Description |
|---|---|---|
source | T | () => T | Value or getter to watch |
callback | (value, oldValue, onCleanup) => void | Called on change |
options.immediate | boolean | Run immediately (default: false) |
options.deep | boolean | number | Deep watch (default: false) |
options.once | boolean | Run once then stop |
Returns
WatchHandle - Object with stop(), pause(), resume() methods.
Like effect, a watch created directly in a component's setup body stops automatically on unmount; pause()/resume() remain useful in the meantime. See Effect cleanup.
Examples
import { signal, watch } from 'sigx';
const state = signal({ count: 0 });
// Basic watch
const handle = watch(
() => state.count,
(newVal, oldVal) => {
console.log(`Changed from ${oldVal} to ${newVal}`);
}
);
state.count = 5; // Logs: "Changed from 0 to 5"
// With cleanup
watch(
() => state.count,
(value, _, onCleanup) => {
const timer = setInterval(() => console.log(value), 1000);
onCleanup(() => clearInterval(timer));
}
);
// Pause/resume
handle.pause();
state.count = 10; // No log
handle.resume(); // Catches up if value changed
// Stop watching
handle.stop();
batch
Group multiple updates into a single reactive flush.
function batch(fn: () => void): void;
Parameters
| Name | Type | Description |
|---|---|---|
fn | () => void | Function containing updates |
Examples
import { signal, effect, batch } from 'sigx';
const state = signal({ a: 0, b: 0, c: 0 });
effect(() => {
console.log(state.a, state.b, state.c);
});
// Without batch: effect runs 3 times
state.a = 1;
state.b = 2;
state.c = 3;
// With batch: effect runs once
batch(() => {
state.a = 1;
state.b = 2;
state.c = 3;
});
untrack
Read values without creating dependencies.
function untrack<T>(fn: () => T): T;
Parameters
| Name | Type | Description |
|---|---|---|
fn | () => T | Function to execute |
Returns
The return value of fn.
Examples
import { signal, effect, untrack } from 'sigx';
const state = signal({ count: 0, multiplier: 2 });
effect(() => {
// Depends on count, NOT multiplier
const mult = untrack(() => state.multiplier);
console.log(state.count * mult);
});
state.count++; // Effect runs
state.multiplier++; // Effect does NOT run
toRaw
Get the underlying non-reactive object from a proxy.
function toRaw<T>(observed: T): T;
Parameters
| Name | Type | Description |
|---|---|---|
observed | T | Reactive proxy or any value |
Returns
The raw object, or the value itself if not reactive.
Examples
import { signal, toRaw } from 'sigx';
const state = signal({ name: 'John', items: [1, 2, 3] });
// Get raw for serialization
const raw = toRaw(state);
fetch('/api', { body: JSON.stringify(raw) });
// Check identity
const obj = { x: 1 };
const reactive = signal(obj);
toRaw(reactive) === obj; // true
isReactive
Check if a value is a reactive proxy.
function isReactive(value: unknown): boolean;
Parameters
| Name | Type | Description |
|---|---|---|
value | unknown | Value to check |
Returns
true if the value is a reactive proxy.
Examples
import { signal, isReactive } from 'sigx';
const state = signal({ count: 0 });
const plain = { count: 0 };
isReactive(state); // true
isReactive(plain); // false
effectScope
Create a scope for grouping effects.
function effectScope(detached?: boolean): EffectScope;
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
detached | boolean | false | Independent of parent scope |
Returns
EffectScope with:
run<T>(fn: () => T): T | undefined- Execute within scopestop(): void- Stop all effects in scope
Examples
import { signal, effect, effectScope } from 'sigx';
const scope = effectScope();
scope.run(() => {
const state = signal({ count: 0 });
effect(() => console.log(state.count));
effect(() => console.log(state.count * 2));
});
// Stop all effects at once
scope.stop();
toSignal
Create a signal-shaped ({ value }) live view over one property of a reactive object. Unlike destructuring — which snapshots the value — the view reads and writes through to the source, so tracking and triggering are preserved.
function toSignal<T extends object, K>(source: T, key: K): PropertySignal<T[K]>;
Parameters
| Name | Type | Description |
|---|---|---|
source | T | A reactive object (e.g. from signal({...})) |
key | K | The property to view (any string key except $set) |
Examples
import { signal, toSignal } from 'sigx';
const state = signal({ count: 0 });
const count = toSignal(state, 'count');
count.value++; // triggers effects watching state.count
console.log(state.count); // 1
toSignals
Create signal-shaped views for every own enumerable property of a reactive object, so it can be destructured without losing reactivity.
function toSignals<T extends object>(source: T): ToSignals<T>;
Parameters
| Name | Type | Description |
|---|---|---|
source | T | A reactive object |
Returns
A ToSignals<T> — one PropertySignal per own enumerable string key ($set excluded).
Examples
import { signal, toSignals } from 'sigx';
const state = signal({ count: 0, name: 'Ada' });
const { count, name } = toSignals(state);
count.value++; // still reactive
name.value = 'Grace'; // writes through to state.name
Types
Signal<T>
type Signal<T> = T & {
$set: (newValue: T) => void;
};
PrimitiveSignal<T>
type PrimitiveSignal<T> = Signal<{ value: Widen<T> }>;
EffectRunner
interface EffectRunner<T = void> {
(): T;
stop: () => void;
}
Computed<T>
interface Computed<T> {
(): T;
}
WritableComputed<T>
interface WritableComputed<T> {
(): T;
set(value: T): void;
}
WatchHandle
interface WatchHandle {
(): void; // Callable to stop
stop: () => void;
pause: () => void;
resume: () => void;
}
EffectScope
type EffectScope = {
run<T>(fn: () => T): T | undefined;
stop(fromParent?: boolean): void;
}
