API
Core API
Section titled “Core API”signal(initialValue)
Section titled “signal(initialValue)”Creates a new signal with the given initial value.
const name = signal('John');console.log(name.value); // "John"name.value = 'Jane';console.log(name.value); // "Jane"computed(deps, computeFn)
Section titled “computed(deps, computeFn)”Creates a computed signal that derives its value from other signals.
const firstName = signal('John');const lastName = signal('Doe');
const fullName = computed([firstName, lastName], async (first, last) => { return `${first} ${last}`;});
console.log(fullName.value); // "John Doe"firstName.value = 'Jane';console.log(fullName.value); // "Jane Doe"effect(signal, fn)
Section titled “effect(signal, fn)”Creates an effect that runs when a signal’s value changes.
const user = signal({ name: 'John', age: 30 });
const unsubscribe = effect(user, (value) => { console.log(`User updated: ${value.name}, ${value.age}`);});
user.update(u => ({ ...u, age: 31 })); // Logs: "User updated: John, 31"
// Later, to clean up:unsubscribe();createEffect(fn)
Section titled “createEffect(fn)”Creates an effect that automatically tracks signal dependencies.
const count = signal(0);const doubled = computed(count, async (value) => value * 2);
const cleanup = createEffect(() => { console.log(`Count: ${count.value}, Doubled: ${doubled.value}`);});
count.value = 5; // Logs: "Count: 5, Doubled: 10"
// Later, to clean up:cleanup();batch(fn)
Section titled “batch(fn)”Batches multiple signal updates to prevent intermediate re-renders.
const firstName = signal('John');const lastName = signal('Doe');const age = signal(30);
// Without batching, this would trigger 3 separate updatesawait batch(async () => { firstName.value = 'Jane'; lastName.value = 'Smith'; age.value = 28;});// Only one update happens after all changes are applieduntrack(fn)
Section titled “untrack(fn)”Runs a function without tracking dependencies.
const count = signal(0);
createEffect(() => { // This will NOT create a dependency on count const value = untrack(() => count.value); console.log(`The count is ${value} (but won't update)`);
// This WILL create a dependency console.log(`The count is ${count.value} (and will update)`);});DOM integration
Section titled “DOM integration”import { signal } from 'signalle';import { bind, bindAttribute, bindClass } from 'signalle/dom';
// Create a two-way binding with an input elementconst nameInput = document.querySelector('#name-input');const nameSignal = bind(nameInput, { property: 'value', events: ['input'], twoWay: true});
// Bind a signal to an attributeconst imageElement = document.querySelector('#profile-image');const imageSrc = signal('default.jpg');bindAttribute(imageElement, 'src', imageSrc);
// Bind a signal to a classconst themeToggle = signal(false);bindClass(document.body, 'dark-theme', themeToggle);Available bindings:
bind(element, options)— basic element bindingbindAll(bindings)— bind multiple elements at oncecomputedBind(element, deps, computeFn, options)— bind a computed valuebindAttribute(element, attribute, signal, render)— bind to an attributebindClass(element, className, signal)— toggle a classbindStyle(element, property, signal, unit)— bind to a style propertybindList(element, itemsSignal, renderItem)— efficient list rendering
Server-side streaming
Section titled “Server-side streaming”import { signal } from 'signalle';import { toSSEResponse } from 'signalle/stream';
const feed = signal({ count: 0 });
// Returns a Response with Content-Type: text/event-stream// that automatically pushes updates when the signal changesconst response = toSSEResponse(feed, { event: 'update', // SSE event name (optional) cors: true // Set CORS headers (optional, default false)});toSSEResponse(signal, options?)
Section titled “toSSEResponse(signal, options?)”Creates a Server-Sent Events Response from a signal. The stream pushes a
new SSE message whenever the signal’s value changes.
Options:
transform(function) — transform the value before sending (default:JSON.stringify)event(string) — SSE event namesendInitial(boolean) — whether to send the signal’s current value immediately (default:true)cors(boolean or string origin) — set CORS headers
toReadableStream(signal, options?)
Section titled “toReadableStream(signal, options?)”The lower-level primitive behind toSSEResponse: converts a signal into a
plain ReadableStream<string> of SSE-formatted chunks, for cases where you
need the stream itself rather than a full Response.
Scoped signals
Section titled “Scoped signals”import { createScope } from 'signalle/scope';
const scope = createScope();
const count = scope.signal(0);const doubled = scope.computed(count, async (v) => v * 2);
const cleanup = scope.createEffect(() => { console.log(`Count: ${count.value}, Doubled: ${doubled.value}`);});
count.value = 5; // Logs: "Count: 5, Doubled: 10"
scope.dispose(); // Cleans up all effects created in this scopecreateScope()
Section titled “createScope()”Creates an isolated signal context, safe for multi-tenant / concurrent server use (e.g. one scope per request). Returns:
scope.signal(initialValue)— scope-isolatedsignal()scope.computed(deps, fn)— scope-isolatedcomputed()scope.effect(signal, fn)— scope-isolatedeffect()scope.createEffect(fn)— scope-isolatedcreateEffect()(see warning below)scope.batch(fn)— scope-isolatedbatch()scope.untrack(fn)— scope-isolateduntrack()scope.dispose()— cleans up every effect created in this scope
Auto-tracking isolation: use scope.createEffect, not the top-level createEffect
Section titled “Auto-tracking isolation: use scope.createEffect, not the top-level createEffect”Signals, computeds, effect(), batch(), and untrack() created through
a scope are fully isolated from every other scope — they hold their own
independent state.
The top-level createEffect imported from signalle is different:
its automatic dependency tracking is implemented with a single tracker
shared by the whole process (a module-static field). Because of that, it
will not auto-track signals created via scope.signal()/
scope.computed() — reading a scoped signal inside a plain, top-level
createEffect(...) simply won’t register a dependency. This is
intentional (it’s what prevents scopes from leaking into each other), but
it means the top-level createEffect must never be used as your
auto-tracking mechanism for scoped signals, and is unsafe as an isolation
boundary between concurrent logical contexts (e.g. two concurrent server
requests) in general.
Use scope.createEffect(fn) instead whenever you need auto-tracking
inside a scope. It has its own independent tracker state per scope, so
multiple scopes’ createEffect calls — even interleaved within the same
event-loop tick — never corrupt each other’s dependency tracking.
Broadcast signals
Section titled “Broadcast signals”Signals that stay in sync across browser tabs, iframes, or workers via
BroadcastChannel.
import { createBroadcastSignal } from 'signalle/broadcast';
// Every `createBroadcastSignal(initial, channelName)` call that shares the// same channel name — in any tab, iframe, or worker — stays in sync.const sharedCount = createBroadcastSignal(0, 'shared-count');
sharedCount.subscribe((value) => { console.log('Count is now:', value);});
// Setting the value here also updates every other tab/worker listening on// the 'shared-count' channel.sharedCount.value = 1;
// Clean up: closes the underlying BroadcastChannelsharedCount.dispose();createBroadcastSignal(initialValue, channelName?)
Section titled “createBroadcastSignal(initialValue, channelName?)”Creates a signal backed by a BroadcastChannel. Values are
structuredCloned before being compared/stored/broadcast, so they must be
structured-clone-safe (plain objects, arrays, primitives, etc. — no
functions or DOM nodes).
initialValue— the signal’s starting value (local to this instance until the first broadcast is received)channelName(optional) — theBroadcastChannelname to synchronize on; defaults to'default-signal'
Returns an object with value (get/set), subscribe(fn), and
dispose().
generateWorkerCode(signalCode, name?)
Section titled “generateWorkerCode(signalCode, name?)”Generates a self-contained string of worker code that embeds the
BroadcastSignal implementation plus a createBroadcastSignal-equivalent
factory (bound to name, default 'createBroadcastSignal'), so it can be
dropped into a Worker/Blob URL without a bundler:
import { generateWorkerCode } from 'signalle/broadcast';
const workerCode = generateWorkerCode(` const sharedCount = createBroadcastSignal(0, 'shared-count'); sharedCount.subscribe((value) => postMessage(value));`);
const blob = new Blob([workerCode], { type: 'application/javascript' });const worker = new Worker(URL.createObjectURL(blob));Before using generateWorkerCode(), read the Security
model page. It performs plain string
interpolation into a JS source string with no sandboxing — equivalent to
eval() with a Worker/Blob indirection on top.
Exports
Section titled “Exports”| Export | Description |
|---|---|
signalle |
Core: signal, computed, effect, createEffect, batch, untrack |
signalle/dom |
DOM bindings: bind, bindAll, computedBind, bindAttribute, bindClass, bindStyle, bindList |
signalle/stream |
Server: toReadableStream, toSSEResponse |
signalle/scope |
Isolation: createScope (SignalScope) |
signalle/broadcast |
Cross-tab/worker sync: createBroadcastSignal, generateWorkerCode |