Reactivity API#

Core reactive primitives for state management.

signal#

Create reactive state. Primitives are wrapped in { value }, objects become reactive proxies.

TSX
function signal<T extends Primitive>(target: T): PrimitiveSignal<T>;
function signal<T extends object>(target: T): Signal<T>;

Parameters#

NameTypeDescription
targetTInitial value (primitive or object)

Returns#

  • Primitives: { value: T } wrapper with reactive value property
  • Objects: Reactive proxy with direct property access

Examples#

TSX
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.

TSX
function effect(fn: EffectFn): EffectRunner;

Parameters#

NameTypeDescription
fn() => voidFunction 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#

TSX
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.

TSX
function computed<T>(getter: () => T): Computed<T>;
function computed<T>(options: { get: () => T; set: (v: T) => void }): WritableComputed<T>;

Parameters#

NameTypeDescription
getter() => TFunction that computes the value
options.get() => TGetter for writable computed
options.set(v: T) => voidSetter for writable computed

Returns#

  • Read-only: Computed<T> - Call to get value: computed()
  • Writable: WritableComputed<T> - Has set() method

Examples#

TSX
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.

TSX
function watch<T>(
    source: WatchSource<T>,
    callback: WatchCallback<T>,
    options?: WatchOptions
): WatchHandle;

Parameters#

NameTypeDescription
sourceT | () => TValue or getter to watch
callback(value, oldValue, onCleanup) => voidCalled on change
options.immediatebooleanRun immediately (default: false)
options.deepboolean | numberDeep watch (default: false)
options.oncebooleanRun 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#

TSX
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.

TSX
function batch(fn: () => void): void;

Parameters#

NameTypeDescription
fn() => voidFunction containing updates

Examples#

TSX
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.

TSX
function untrack<T>(fn: () => T): T;

Parameters#

NameTypeDescription
fn() => TFunction to execute

Returns#

The return value of fn.

Examples#

TSX
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.

TSX
function toRaw<T>(observed: T): T;

Parameters#

NameTypeDescription
observedTReactive proxy or any value

Returns#

The raw object, or the value itself if not reactive.

Examples#

TSX
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.

TSX
function isReactive(value: unknown): boolean;

Parameters#

NameTypeDescription
valueunknownValue to check

Returns#

true if the value is a reactive proxy.

Examples#

TSX
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.

TSX
function effectScope(detached?: boolean): EffectScope;

Parameters#

NameTypeDefaultDescription
detachedbooleanfalseIndependent of parent scope

Returns#

EffectScope with:

  • run<T>(fn: () => T): T | undefined - Execute within scope
  • stop(): void - Stop all effects in scope

Examples#

TSX
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.

TSX
function toSignal<T extends object, K>(source: T, key: K): PropertySignal<T[K]>;

Parameters#

NameTypeDescription
sourceTA reactive object (e.g. from signal({...}))
keyKThe property to view (any string key except $set)

Examples#

TSX
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.

TSX
function toSignals<T extends object>(source: T): ToSignals<T>;

Parameters#

NameTypeDescription
sourceTA reactive object

Returns#

A ToSignals<T> — one PropertySignal per own enumerable string key ($set excluded).

Examples#

TSX
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>#

TSX
type Signal<T> = T & {
    $set: (newValue: T) => void;
};

PrimitiveSignal<T>#

TSX
type PrimitiveSignal<T> = Signal<{ value: Widen<T> }>;

EffectRunner#

TSX
interface EffectRunner<T = void> {
    (): T;
    stop: () => void;
}

Computed<T>#

TSX
interface Computed<T> {
    (): T;
}

WritableComputed<T>#

TSX
interface WritableComputed<T> {
    (): T;
    set(value: T): void;
}

WatchHandle#

TSX
interface WatchHandle {
    (): void;          // Callable to stop
    stop: () => void;
    pause: () => void;
    resume: () => void;
}

EffectScope#

TSX
type EffectScope = {
    run<T>(fn: () => T): T | undefined;
    stop(fromParent?: boolean): void;
}