Skip to content

API Overview

SymbolPurposeExecution modeCommon gotcha
createRipple()Create isolated graphSyncDisposal is terminal; create a new graph instead of reusing it
signal()Create writable valueSyncDefault graph is process-wide
computed()Create lazy derived valueSyncKeep derivation pure
effect()React to dependency readsSyncDispose handle or return cleanup
batch()Coalesce synchronous writesSyncDoes not roll back writes
createScope()Group owned reactive workSyncCall run() to activate it
untrack()Read without trackingSyncRead still happens immediately
watch()Observe selected outputSyncUse effect() for broad reads
resource()Load async sourceAsyncRead dependencies in source callback
isReactive()Test Readable identitySyncDoes not test arbitrary objects

Package Entry Point

ImportPurpose
@vielzeug/rippleAll primitives, types, and errors — signals, computed, effects, scopes, watch, resource, and the isolated graph factory

Graph Creation

createRipple(options?)

ts
function createRipple(options?: RippleOptions): Ripple;

Creates one isolated reactive graph. Factories on the returned object share scheduling, ownership, observer, and error boundaries. dispose() is terminal: ripple.disposed becomes true, existing owned work is disposed, and creating more graph work throws RippleDisposedRuntimeError. Create a new graph for a new lifetime.

ParameterTypeDescription
options.onError(error, context) => voidReceives effect, cleanup, listener, or observer failures.
options.observerReactiveObserverReceives graph events.

Returns: Ripple.

Example:

ts
import { createRipple } from '@vielzeug/ripple';

const ripple = createRipple();
const count = ripple.signal(0);
const stop = ripple.effect(() => console.log(count.value));

stop.dispose();
ripple.dispose();

isReactive(value)

ts
function isReactive<T>(value: T | Readable<T>): value is Readable<T>;

Tests whether a value is a Ripple-created readable node, including Resource. Recognition works across duplicated Ripple module graphs.

Returns: true for a Ripple Signal, computed value, or Resource; otherwise false.

Example:

ts
import { isReactive, signal } from '@vielzeug/ripple';

console.log(isReactive(signal(0)));

Default Graph Functions

signal(initial, options?)

ts
function signal<T>(initial: T, options?: SignalOptions<T>): Signal<T>;

Creates writable state on the default graph. Use update() for immutable replacement patterns.

Returns: Signal<T>.

Example:

ts
import { signal } from '@vielzeug/ripple';

const count = signal(0);
count.value += 1;

const cart = signal({ items: 0 });
cart.update((state) => ({ ...state, items: state.items + 1 }));

computed(derive, options?)

ts
function computed<T>(derive: () => T, options?: ComputedOptions<T>): Readable<T>;

Creates a lazy read-only value from reactive reads in derive.

Returns: Readable<T>.

Example:

ts
import { computed, signal } from '@vielzeug/ripple';

const count = signal(2);
const doubled = computed(() => count.value * 2);
console.log(doubled.value);

effect(callback, options?)

ts
function effect(callback: () => Cleanup | undefined, options?: EffectOptions): EffectHandle;

Runs immediately and reruns when its tracked reads change. A returned cleanup runs before the next callback or disposal.

Returns: EffectHandle.

Example:

ts
import { effect, signal } from '@vielzeug/ripple';

const connected = signal(false);
const stop = effect(() => {
  if (!connected.value) return;

  return () => console.log('disconnect');
});

stop.dispose();

batch(fn) and untrack(fn)

ts
function batch<T>(fn: () => T): T;
function untrack<T>(fn: () => T): T;

batch() defers effects and listeners until its callback returns. untrack() reads current state without adding dependencies to an enclosing effect.

Returns: the callback result.

Example:

ts
import { batch, signal, untrack } from '@vielzeug/ripple';

const first = signal('Ada');
const last = signal('Lovelace');
const locale = signal('en-US');

batch(() => {
  first.value = 'Grace';
  last.value = 'Hopper';
});

console.log(untrack(() => locale.value));

createScope(name?)

ts
function createScope(name?: string): Scope;

Creates a disposable ownership boundary. Work created inside scope.run() belongs to that scope.

Returns: Scope.

Example:

ts
import { createScope, effect, signal } from '@vielzeug/ripple';

const scope = createScope('panel');
const count = signal(0);

scope.run(() => effect(() => console.log(count.value)));
scope.dispose();

Watch and Resources

watch(source, callback, options?)

ts
function watch<T>(
  source: Readable<T> | (() => T),
  callback: (value: T, previous: T | undefined) => void,
  options?: WatchOptions<T>,
): EffectHandle;

Observes selected output changes using the default graph or a Ripple.watch() method.

Returns: EffectHandle.

Example:

ts
import { signal, watch } from '@vielzeug/ripple';

const count = signal(0);
const stop = watch(count, (value, previous) => console.log(previous, value), { immediate: true });
stop.dispose();

resource(source, loader, options?)

ts
function resource<Source, Value>(
  source: () => Source,
  loader: (source: Source, context: { readonly signal: AbortSignal }) => Promise<Value>,
  options?: ResourceOptions,
): Resource<Value>;

Tracks source, aborts stale loader work, and exposes AsyncState<Value>. Source and loader failures become status: 'error' state; handle them from resource.value rather than RippleOptions.onError, which is reserved for runtime callback, cleanup, listener, and observer failures.

Returns: Resource<Value>.

Example:

ts
import { resource, signal } from '@vielzeug/ripple';

const userId = signal('42');
const user = resource(() => userId.value, async (id) => ({ id }));

if (user.value.status === 'error') console.error(user.value.error);
user.dispose();

Types

ts
type Cleanup = () => void;
type Equality<T> = (previous: T, next: T) => boolean;
type Unsubscribe = () => void;

type SignalOptions<T> = { equals?: Equality<T>; name?: string };
type ComputedOptions<T> = { equals?: Equality<T>; name?: string };
type EffectOptions = { name?: string; scheduler?: 'microtask' | 'sync' };
type WatchOptions<T> = { equals?: Equality<T>; immediate?: boolean; name?: string; once?: boolean };
type ResourceOptions = { name?: string };

type ReactiveEvent =
  | { readonly kind: 'compute'; readonly name?: string }
  | { readonly kind: 'effect'; readonly name?: string }
  | { readonly kind: 'write'; readonly name?: string; readonly next: unknown; readonly previous: unknown }
  | { readonly kind: 'dispose'; readonly name?: string; readonly node: 'effect' | 'scope' };

type ReactiveObserver = (event: ReactiveEvent) => void;
type ReactiveErrorContext = { readonly kind: 'cleanup' | 'effect' | 'listener' | 'observer'; readonly name?: string };
type RippleOptions = { observer?: ReactiveObserver; onError?: (error: unknown, context: ReactiveErrorContext) => void };

type AsyncState<T> =
  | { readonly previous?: T; readonly status: 'pending' }
  | { readonly status: 'success'; readonly value: T }
  | { readonly error: unknown; readonly previous?: T; readonly status: 'error' };

interface Readable<T> {
  readonly name?: string;
  peek(): T;
  subscribe(listener: () => void): Unsubscribe;
  readonly value: T;
}

interface Signal<T> extends Readable<T> { update(updater: (prev: T) => T): void; value: T }
interface Disposable { dispose(): void; readonly disposed: boolean; readonly disposalSignal: AbortSignal; [Symbol.dispose](): void }
type EffectHandle = Disposable;
interface Scope extends Disposable { run<T>(fn: () => T): T }

interface Resource<T> extends Readable<AsyncState<T>>, Disposable { reload(): void }

interface Ripple {
  batch<T>(fn: () => T): T;
  computed<T>(derive: () => T, options?: ComputedOptions<T>): Readable<T>;
  createScope(name?: string): Scope;
  dispose(): void;
  readonly disposed: boolean;
  effect(callback: () => Cleanup | void, options?: EffectOptions): EffectHandle;
  resource<Source, Value>(source: () => Source, loader: (source: Source, context: { readonly signal: AbortSignal }) => Promise<Value>, options?: ResourceOptions): Resource<Value>;
  signal<T>(initial: T, options?: SignalOptions<T>): Signal<T>;
  untrack<T>(fn: () => T): T;
  watch<T>(source: Readable<T> | (() => T), callback: (value: T, previous: T | undefined) => void, options?: WatchOptions<T>): EffectHandle;
}

Errors

ErrorTriggerNotable properties
RippleErrorBase Ripple errorUse instanceof RippleError to narrow unknown values.
RippleComputedCycleErrorComputed dependency reads itself through a cycleExtends RippleError.
RippleDisposedRuntimeErrorFactory or execution API used after ripple.dispose()Extends RippleError.
RippleDisposedScopeErrorscope.run() after scope disposalExtends RippleError.
RippleInfiniteLoopErrorEffect flush exceeds graph iteration limitExtends RippleError.