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 });

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.

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.

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();

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;
}