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.

ts
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.

ts
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 microtask

ComputedSignal

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.

ts
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 done

EffectSignal

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().

ts
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.

ts
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.

ts
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 signal
Note

Both 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 for delay ms.
  • createFilteredSignal(source, predicate) — emits only values that pass predicate.
  • createMappedSignal(source, mapFn) — emits mapFn(value) for every source emission.

Scheduling & lifecycle

  • Microtask batching: StateSignal and EventSignal notify on a microtask, coalescing multiple synchronous mutations within the same tick.
  • Macrotask batching: ComputedSignal and EffectSignal recompute on a macrotask, collapsing several dependency changes into a single recalculation.
  • Subscription options: {once, priority, receivePrevious} — receivePrevious (default true on StateSignal) controls whether a new subscriber is immediately called with the current value.
  • Cleanup rule: StateSignal/EventSignal don't require disposal unless you want earlier GC; ComputedSignal and EffectSignal hold live subscriptions to their dependencies and must be destroyed.