Documentation
Signals
Fine-grained reactive state — State, Computed, Effect, Event, and Channel.
Signals are Alwatr's reactivity primitive — small, composable objects that hold a value (or nothing at all, for events) and notify subscribers when that value changes. There are five core primitives plus two persistence variants, all from @alwatr/signal and re-exported in full by @alwatr/flux.
StateSignal
The foundation. Holds a mutable value; new subscribers receive the current value immediately by default.
import {createStateSignal} from '@alwatr/flux';
const counter = createStateSignal({name: 'app-counter', initialValue: 0});
const subscription = counter.subscribe((value) => {
console.log(`Counter updated to: ${value}`);
});
counter.set(1); // → "Counter updated to: 1"
counter.update((current) => current + 1); // → "Counter updated to: 2"
subscription.unsubscribe();createStateSignal(...) is the preferred factory (better type inference than new StateSignal(...), though both are exported).
EventSignal
Stateless. For transient events with no persistent value — subscribers only ever see emissions that happen after they subscribe.
import {createEventSignal} from '@alwatr/flux';
const clickSignal = createEventSignal<{x: number; y: number}>({name: 'user-click'});
clickSignal.subscribe((pos) => console.log(`Clicked at ${pos.x}, ${pos.y}`));
clickSignal.dispatch({x: 150, y: 300}); // dispatched on a microtaskComputedSignal
A read-only, memoized value derived from other signals. Recalculates only when a declared dependency changes — and must be destroyed when no longer needed, since it holds live subscriptions to its dependencies.
import {createStateSignal, createComputedSignal} from '@alwatr/flux';
const firstName = createStateSignal({name: 'first-name', initialValue: 'John'});
const lastName = createStateSignal({name: 'last-name', initialValue: 'Doe'});
const fullName = createComputedSignal({
name: 'full-name',
deps: [firstName, lastName],
get: () => `${firstName.get()} ${lastName.get()}`,
});
console.log(fullName.get()); // "John Doe"
firstName.set('Jane'); // recomputes on the next macrotask
fullName.destroy(); // always destroy when doneEffectSignal
The bridge to side-effects: logging, DOM writes, network requests — anything that reacts to signal changes without itself being a value other signals depend on. Also requires .destroy().
import {createStateSignal, createEffect} from '@alwatr/flux';
const count = createStateSignal({name: 'count', initialValue: 0});
const logEffect = createEffect({
name: 'log-effect',
deps: [count],
run: () => console.log(`The count changed to: ${count.get()}`),
runImmediately: true, // run once on creation, not just on the first change
});
count.set(5);
logEffect.destroy();ChannelSignal
A single, typed multi-message bus — one channel routing many named message types in O(1) via an internal Map<name, Set<handler>>. This is the primitive the action bus (Actions & Events) is built on top of; reach for it directly whenever you'd otherwise create a pile of individual EventSignals.
import {createChannelSignal} from '@alwatr/flux';
interface AppMessages {
'open-drawer': {panel: string};
'close-drawer': void;
}
const appChannel = createChannelSignal<AppMessages>({name: 'app-channel'});
appChannel.on('open-drawer', (payload) => console.log(payload.panel));
appChannel.dispatch('open-drawer', {panel: 'settings'});Persistent & Session signals
PersistentStateSignal and SessionStateSignal extend StateSignal with automatic localStorage / sessionStorage persistence — debounced writes, schema versioning, and full BFCache lifecycle integration. See Storage & Hydration for the full picture, including SSR hydration.
import {createPersistentStateSignal} from '@alwatr/flux';
const prefs = createPersistentStateSignal({
name: 'user-prefs',
schemaVersion: 1,
initialValue: {theme: 'light'},
saveDebounceDelay: 500,
});
prefs.set({theme: 'dark'}); // auto-saved (debounced) to localStorage
prefs.remove(); // clear storage without destroying the signalBoth a class constructor (new PersistentStateSignal(...)) and a factory (createPersistentStateSignal(...)) are exported — pick one and stay consistent within a codebase.
Operators
Operators take a source signal and return a new, computed-style signal — each one must also be .destroy()d:
createDebouncedSignal(source, {delay})— emits only after the source has been quiet fordelayms.createFilteredSignal(source, predicate)— emits only values that passpredicate.createMappedSignal(source, mapFn)— emitsmapFn(value)for every source emission.
Scheduling & lifecycle
- Microtask batching:
StateSignalandEventSignalnotify on a microtask, coalescing multiple synchronous mutations within the same tick. - Macrotask batching:
ComputedSignalandEffectSignalrecompute on a macrotask, collapsing several dependency changes into a single recalculation. - Subscription options:
{once, priority, receivePrevious}—receivePrevious(defaulttrueonStateSignal) controls whether a new subscriber is immediately called with the current value. - Cleanup rule:
StateSignal/EventSignaldon't require disposal unless you want earlier GC;ComputedSignalandEffectSignalhold live subscriptions to their dependencies and must be destroyed.