Skip to content

Effects API

Effects in a scope subscribe to observable reads and run work when those reads change. Three primitives cover the common cases.

All three register against the current scope and are automatically disposed when the scope tears down. Use them inside "use scope" blocks, useScope factories, or store setup functions.

Run a callback whenever any observable read inside it changes. Tracks reads on every run.

Signature:

function observe(callback: () => void): () => void;

Returns a dispose function. Inside a scope the dispose is auto-registered — you do not need to capture the return value.

Example:

import { observable, observe } from "@usels/core";
function Counter() {
"use scope";
const count$ = observable(0);
observe(() => {
document.title = `Count: ${count$.get()}`;
});
return <button onClick={() => count$.set((c) => c + 1)}>{count$.get()}</button>;
}

The callback runs immediately, then re-runs whenever count$ (or any other observable read inside it) changes.

Subscribe to a selector and react to its changes. By default, does not fire on the initial value — only on subsequent changes (immediate: false).

Signature:

function watch<T extends WatchSource>(
selector: T,
effect: Effector<T>,
options?: WatchOptions,
): Disposable;
interface WatchOptions {
/** Fire effect on mount when `true`. @default false */
immediate?: boolean;
/** `'sync'` fires synchronously inside a batch; `'deferred'` waits until the batch ends. */
schedule?: "sync" | "deferred";
}

WatchSource accepts an Observable<T>, a () => T selector function, or a tuple of either (for multi-source watching).

Example:

import { observable, watch } from "@usels/core";
function SearchSync() {
"use scope";
const query$ = observable("");
watch(query$, (query) => {
console.log("query changed:", query);
});
return <input value={query$.get()} onChange={(event) => query$.set(event.currentTarget.value)} />;
}

Use when you care about a specific source (or composition of sources) and want to skip the initial value by default.

Subscribe to a selector and run the effect every time the value transitions to truthy. With { once: true } it disposes after the first truthy fire.

Signature:

function whenever<T>(
selector: Selector<T>,
effect: (value: Truthy<T>) => void,
options?: WheneverOptions,
): Disposable;
interface WheneverOptions extends WatchOptions {
/** Dispose after the first truthy invocation. @default false */
once?: boolean;
}

Selector<T> is an Observable<T> or a () => T function. Truthy<T> strips false | 0 | "" | null | undefined from T.

Example:

import { observable, whenever } from "@usels/core";
function ReadyBanner() {
"use scope";
const ready$ = observable(false);
whenever(ready$, () => {
console.log("ready!");
});
return <button onClick={() => ready$.set(true)}>Ready</button>;
}

Each transition from falsy → truthy fires the effect. Pass { once: true } if you want the subscription to self-dispose after the first fire.

NeedAPI
React to any observable read inside the effect body (fires on mount)observe()
React to a specific source (or tuple), skip the first value by defaultwatch()
Fire only when a value becomes truthy (optionally one-shot via once)whenever()

Outside a scope (in a regular component body), use the hook wrappers: useObserve(), useWatch(), useWhenever(). They follow the same signatures but integrate with React’s hook lifecycle. See use* vs create* for when to pick which.