API reference

signalium

Functions


signal

function signal<T>(initialValue: T, opts?: SignalOptions<T>): Signal<T>;

Creates a Signal instance of type T.

import { signal } from 'signalium';

const count = signal(0);

// Read
console.log(count.value); // 0

// Write
count.update((v) => v + 1);
count.value = 2; // equivalent direct set
ParameterTypeDescription
initialValueTInitial value for the signal
optsSignalOptions<T>See Types below

reactive

function reactive<T, Args extends unknown[]>(
  fn: (...args: Args) => T,
  opts?: ReactiveOptions<T, Args>,
): ReactiveFn<T, Args>;

Creates a Reactive Function that re-computes when its dependencies change. Async functions return a ReactivePromise.

Reactive Functions cache by parameters and automatically track reads of Signals and other Reactive Functions.

import { signal, reactive } from 'signalium';

const first = signal('Ada');
const last = signal('Lovelace');

const fullName = reactive(() => `${first.value} ${last.value}`);

console.log(fullName()); // "Ada Lovelace"
first.value = 'Grace';
last.value = 'Hopper';
console.log(fullName()); // updates to "Grace Hopper"

// Parameterized reactive function (memoized by paramKey or args)
const power = reactive((base: number) => base * base);
console.log(power(4)); // 16
console.log(power(5)); // 25
ParameterTypeDescription
fn(...args: Args) => TComputation function
optsReactiveOptions<T, Args>See Types below

relay

export function relay<T>(
  activate: RelayActivate<T>,
  opts?: SignalOptions<T>,
): ReactivePromise<T>;

Creates a long-lived reactive promise for event-like async sources. Relays are great for subscriptions, sockets, timers, or any producer that can push multiple updates over time.

import { relay } from 'signalium';

const time = relay<number>((state) => {
  const id = setInterval(() => (state.value = Date.now()), 1000);
  return () => clearInterval(id);
});

console.log(time.isPending); // true initially
setTimeout(() => {
  if (time.isReady) console.log(time.value); // current timestamp
}, 1500);
ParameterTypeDescription
activateRelayActivate<T>Activation function controlling the relay
optsSignalOptions<T>See Types below

RelayActivate

export type RelayActivate<T> = (
  state: RelayState<T>,
) => RelayHooks | (() => unknown) | void | undefined;

Called when a relay is first observed (activated). Use state to push values or errors. Return an object of hooks or a teardown function to manage external resources.

  • Returning { update?(), deactivate?() } lets you signal external changes (update) and clean up on deactivation (deactivate).
  • Returning a function is treated as a teardown equivalent to { deactivate() { /* ... */ } }.
ParameterTypeDescription
stateRelayState<T>Imperative API to push values/errors into the relay

RelayState

export interface RelayState<T> {
  value: T | undefined;
  setPromise: (promise: Promise<T>) => void;
  setError: (error: unknown) => void;
}
PropertyTypeDescription
valueT | undefinedCurrent value snapshot
setPromise(promise: Promise<T>) => voidPush an async result. Updating the relay this way will cause it to go into a pending state until the promise resolves.
setError(error: unknown) => voidPush an error

RelayHooks

export type RelayHooks = {
  update?(): void;
  deactivate?(): void;
};
PropertyTypeDescription
update() => voidUpdate the relay in response to external changes
deactivate() => voidCleanup when the last watcher disconnects

task

export function task<T, Args extends unknown[]>(
  fn: (...args: Args) => Promise<T>,
  opts?: SignalOptions<T>,
): ReactiveTask<T, Args>;

Wraps an async function as a runnable reactive task. Tasks expose run(...args) to start/restart the underlying promise while preserving a single reactive handle. This is essentially a shorthand for running an async function in an event handler and then assigning the resulting promise to a signal.

import { signal, reactive, task } from 'signalium';
import { component } from 'signalium/react';

const updateUser = reactive(async (id: string) => {
  const res = await fetch(`/api/users/${id}`, {
    method: 'PATCH',
    body: JSON.stringify({ name: 'Tony Stark' }),
  });

  return res.json() as Promise<{ id: string; name: string }>;
});

// 1) Manual async + signal
const UserManual = component(() => {
  // Have to create a signal to hold the promise
  const updatePromise = signal<
    ReactivePromise<{ id: string; name: string }> | undefined
  >();

  return (
    <div>
      {/* Assign the promise to the signal */}
      <button onClick={() => (updatePromise.value = updateUser('1'))}>
        Load
      </button>
      {/* Have to unwrap the promise to access the properties */}
      {updatePromise.value?.isPending && <p>Loading…</p>}
      {updatePromise.value?.isRejected && <p>Error</p>}
      {/* Double unwrapping here is pretty ugly 🤮 */}
      {updatePromise.value?.isReady && <p>{updatePromise.value.value.name}</p>}
    </div>
  );
});

// 2) Task-based
const updateUserTaskFor = reactive((id: string) => {
  return task(async (id: string) => {
    const res = await fetch(`/api/users/${id}`, {
      method: 'PATCH',
      body: JSON.stringify({ name: 'Tony Stark' }),
    });

    return res.json() as Promise<{ id: string; name: string }>;
  });
});

const UserTask = component(({ id }: { id: string }) => {
  // The task instance is the promise itself
  const updateUserTask = updateUserTaskFor(id);

  return (
    <div>
      {/* We can run the task directly, no need to assign to a signal */}
      <button onClick={() => updateUserTask.run()}>Update user {id}</button>
      {/* We can access properties directly! */}
      {updateUserTask.isPending && <p>Loading…</p>}
      {updateUserTask.error && <p>Error</p>}
      {updateUserTask.isReady && <p>{updateUserTask.value.name}</p>}
    </div>
  );
});
ParameterTypeDescription
fn(...args: Args) => Promise<T>Async computation
optsSignalOptions<T>See Types below

watcher

export function watcher<T>(
  fn: () => T,
  opts?: SignalOptions<T> & { tracer?: Tracer; isolate?: boolean },
): Watcher<T>;

Subscribes externally to a reactive computation.

import { signal, watcher } from 'signalium';

const count = signal(0);
const w = watcher(() => count.value);

const stop = w.addListener(() => {
  console.log('count changed to', w.value);
});

count.value = 1;
stop();

Watchers are ideal when you need to bridge reactivity to non-reactive environments (DOM, external libs) via listeners. They act as "exit points" for your reactive graph, and are the primary way that Signals are consumed by external consumers. Watchers are also the primary way that relays are activated and deactivated, since nodes only activate when a Watcher is connected to them directly or indirectly.

ParameterTypeDescription
fn() => TReactive producer to watch
optsSignalOptions<T>See Types below

context

export function context<T>(initialValue: T, description?: string): Context<T>;

Create a new Context. Contexts provide dependency injection. Define a Context at the root and read it downstream without threading props.

import { context, withContexts, getContext } from 'signalium';

const Theme = context<'light' | 'dark'>('light');

withContexts([[Theme, 'dark']], () => {
  const value = getContext(Theme);
  console.log(value); // 'dark'
});
ParameterTypeDescription
initialValueTDefault value
descriptionstring?Optional label

getContext

export function getContext<T>(context: Context<T>): T;

Read a Context value.

Use inside reactive computations or within a withContexts/provider scope.

ParameterTypeDescription
contextContext<T>Context handle

withContexts

export function withContexts<C extends unknown[], U>(
  contexts: [...ContextPair<C>],
  fn: () => U,
): U;

Temporarily apply Contexts for a call.

Allows scoping multiple Context values for the duration of a function call.

ParameterTypeDescription
contexts[...ContextPair<C>]Context pairs to apply
fn() => UFunction to execute

reactiveMethod

export function reactiveMethod<T, Args extends unknown[]>(
  owner: object,
  fn: (...args: Args) => T,
  opts?: ReactiveOptions<T, Args>,
): (...args: Args) => T | DiscriminatedReactivePromise<T>;

Like reactive, but bound to an owner object for scoping.

Use when the computation should be cached/owned relative to an object instance (e.g., a Context class) rather than globally. This is useful when you want to ensure that a single global store is not affected by child Contexts, since each time a child Context is overridden, a new scope is created and all Reactive Functions are re-evaluated in the new scope.

import { signal, reactiveMethod } from 'signalium';

class Counter {
  count = signal(0);
  double = reactiveMethod(this, () => this.count.value * 2);
}

const a = new Counter();
const b = new Counter();

a.count.value = 2;
b.count.value = 5;
console.log(a.double()); // 4
console.log(b.double()); // 10

When overriding child Contexts, a normal reactive function will re-read the new Context values, while a reactiveMethod stays bound to its owner's scope and does not.

import {
  context,
  getContext,
  withContexts,
  reactive,
  reactiveMethod,
} from 'signalium';

// A context that affects computations
const Settings = context({ factor: 2 }, 'settings');

// Create an owner object in a parent scope
withContexts(
  [
    [Settings, { factor: 2 }],
    [context<object>({}, 'owner'), {}],
  ],
  () => {
    const owner = getContext(context<object>({}, 'owner'));

    // Regular Reactive Function reads from the current scope
    const normalMultiply = reactive(
      (x: number) => x * getContext(Settings).factor,
    );

    // reactiveMethod is bound to the parent's owner scope
    const methodMultiply = reactiveMethod(
      owner,
      (x: number) => x * getContext(Settings).factor,
    );

    console.log(normalMultiply(10)); // 20 (factor 2)
    console.log(methodMultiply(10)); // 20 (factor 2)

    // Override Settings in a child scope
    withContexts([[Settings, { factor: 3 }]], () => {
      console.log(normalMultiply(10)); // 30 (uses child factor 3)
      console.log(methodMultiply(10)); // 20 (still uses owner scope factor 2)
    });
  },
);
ParameterTypeDescription
ownerobjectObject whose scope owns the method
fn(...args: Args) => TMethod computation
optsReactiveOptions<T, Args>See Types below

setScopeOwner

export function setScopeOwner(owner: object, ownerObject: object): void;

Associate an owner object with another owner (or with an object that is already tied to a scope through a Context). This lets reactiveMethod(owner, ...) resolve to the intended scope even when owner differs from the object registered in a scope.

setScopeOwner is chainable: if you link A → B and B → C, and C is registered as the owner in a scope, then methods bound to A resolve using C's scope.

import {
  context,
  withContexts,
  getContext,
  reactiveMethod,
  setScopeOwner,
} from 'signalium';

const OwnerCtx = context<object | null>(null, 'owner');
const LabelCtx = context('default', 'label');

class Parent {
  method = reactiveMethod(this, () => getContext(LabelCtx));
}

class Child {
  method = reactiveMethod(this, () => getContext(LabelCtx));
}

const parent = new Parent();
const child = new Child();

// Register parent as the owner in a scope
withContexts(
  [
    [OwnerCtx, parent],
    [LabelCtx, 'owned'],
  ],
  () => {},
);

// Map child's owner to the parent's scope
setScopeOwner(child, parent);

child.method(); // 'owned'

Note that this API is a low-level API meant to enable more performant Context development. At some point we will try to add a better high-level abstraction for defining ownership and interdependencies between Contexts and subclasses of contexts. This API, along with reactiveMethod, are meant to unblock development for now until those are developed.

ParameterTypeDescription
objobjectThe owner to associate
ownerobjectAnother owner or an object tied to a scope

notifier

function notifier(opts?: SignalOptions<undefined>): Notifier;

Creates a Notifier, a special zero-value Signal used for manual invalidation. Inside a Reactive Function, call consume() to depend on the Notifier. Later, calling notify() will invalidate dependents so they recompute on next access.

import { notifier, reactive } from 'signalium';

const n = notifier();
let count = 0;

const get = reactive(() => {
  n.consume();
  return count;
});

get(); // 0
count = 1;
get(); // still 0 (notifier not notified yet)
n.notify();
get(); // 1
ParameterTypeDescription
optsSignalOptions<undefined>See Types below

Types


Signal

export interface Signal<T> {
  value: T;
  update(updater: (value: T) => T): void;
}
PropertyTypeDescription
valueTCurrent value
update(updater: (value: T) => T)Update in place without consuming the previous value

SignalOptions

export interface SignalOptions<T> {
  equals?: (prev: T, next: T) => boolean | false;
  id?: string;
  desc?: string;
}
PropertyTypeDescription
equals((prev: T, next: T) => boolean) | falseCustom equality (false forces updates)
idstringDebug identifier
descstringDebug description

ReactiveFn

export type ReactiveFn<T, Args extends unknown[]> = (
  ...args: Args
) => T extends Promise<infer U> ? ReactivePromise<U> : T;
ParameterTypeDescription
...argsArgsFunction arguments

ReactiveOptions

export interface ReactiveOptions<T, Args extends unknown[]>
  extends SignalOptions<T> {
  paramKey?: (...args: Args) => string | number;
}
PropertyTypeDescription
equals((prev: T, next: T) => boolean) | falseInherited from SignalOptions
idstringInherited from SignalOptions
descstringInherited from SignalOptions
paramKey(...args: Args) => string | numberKey to group parameterized instances

ReactivePromise

export interface ReactivePromise<T> extends Promise<T> {
  readonly value: T | undefined;
  readonly error: unknown;

  readonly isPending: boolean;
  readonly isRejected: boolean;
  readonly isResolved: boolean;
  readonly isSettled: boolean;
  readonly isReady: boolean;
}
PropertyTypeDescription
valueT | undefinedCurrent resolved value
errorunknownCurrent error if rejected
isPendingbooleanPromise is pending
isRejectedbooleanPromise was rejected
isResolvedbooleanPromise was resolved
isSettledbooleanPromise is settled
isReadybooleanValue is available (non-undefined)
Previous
Code Transforms and Async Context