Skip to content

Index

SynState provides simple, intuitive APIs for managing application state:

Built-in event emitter for event-driven patterns:

For complex scenarios, SynState provides observable-based APIs:

  • source<T>(): Create a new observable source (almost equivalent to RxJS subject).
  • just(value): Create an Observable that holds a single static value and immediately completes. Useful as a synchronous fallback inside switchMap.
  • empty(): Create an Observable that holds no value and is already completed (almost equivalent to RxJS EMPTY). Useful for the branch of a switchMap that should follow nothing.
  • fromPromise(promise): Create observable from promise.
  • fromAbortablePromise(factory): Create observable from a promise factory that receives an AbortSignal, aborted when the observable completes (as when switchMap switches away).
  • fromSubscribable(): Create observable from any subscribable object.
  • counter(ms): Emit 0 at once, then 1, 2, … at intervals (almost equivalent to RxJS timer(0, ms); with { emitInitial: false }, almost equivalent to interval(ms); { times: n } completes it after n values).
  • timer(delay): Emit after delay.
  • mergeMap(fn): Map to observables and merge all (runs in parallel) (alias: flatMap).
  • switchMap(fn): Map to observables and switch to latest (cancels previous).
  • audit(ms): Emit the last value after specified time window (almost equivalent to RxJS auditTime).
  • debounce(ms): Debounce emissions (almost equivalent to RxJS debounceTime).
  • throttle(ms): Throttle emissions (almost equivalent to RxJS throttleTime).

For libraries built on SynState, such as a UI binding that mounts and unmounts parts of a page. Application state defined at module level belongs to no scope and needs none of this.

  • createScope(parent?, { after? }): Create a scope. Observables created synchronously inside scope.run(body) belong to it, and scope.dispose() disposes them without completing the shared observables they were derived from. A subscribe made there belongs to it too, and dispose() unsubscribes it. With parent, it is disposed together with the parent. With after, every observable derived in the scope and its child scopes updates after after, however few steps it is from the source; a switch that mounts a branch in the scope passes the value it switches on, so that it disposes a branch the next value rules out before the branch computes that value.
  • getCurrentScope(): The scope whose run is executing, if any. Take it while still synchronous and wrap work after an await in its run, since only what is created synchronously joins a scope.
  • onScopeDispose(cleanup): Call cleanup when the current scope is disposed.
  • untracked(body): Call body with no current scope, so that what it creates, and what it subscribes, outlives the surrounding scope.

dispose() ends the subscriptions, then disposes the child scopes, then calls the onScopeDispose functions, then disposes the owned observables, each newest first. A subscription made in the scope therefore hears neither a value an onScopeDispose function writes nor the completion of the observables the scope or its children dispose. An update or a completion runs in no scope, whichever scope wrote the value or completed the observable, so what a subscriber subscribes or creates on hearing it belongs to no scope unless the subscriber runs one. run on a disposed scope warns and owns nothing it creates.

  • isChildObservable(obs): Check if observable is a child observable.
  • isManagerObservable(obs): Check if observable is a manager observable.
  • isRootObservable(obs): Check if observable is a root observable.

React / Preact Hooks (synstate-react-hooks / synstate-preact-hooks)

Section titled “React / Preact Hooks (synstate-react-hooks / synstate-preact-hooks)”

Both packages share the same API surface. Use synstate-react-hooks for React 18+ and synstate-preact-hooks for Preact.

  • useObservableValue(observable$): Subscribe to an observable and return its current value as component state. Uses useSyncExternalStore internally — the component re-renders when the observable emits.
  • useObservableValue(observable$, fallback): Same as above, with a fallback value for observables without an initial value.
  • useObservableEffect(observable$, fn): Subscribe fn while the component is mounted (details).
  • useValueAsObservable(value): Convert a component prop/state value into a synstate observable. Useful for bridging React/Preact state into the observable graph. The observable completes when the component unmounts. In React, it is replaced after an <Activity> is hidden and shown again, so read it from each render rather than keeping the first one.

These re-export core createState / createReducer / createBooleanState but return a hook (useCurrentValue) as the first tuple element instead of the raw observable:

  • createState(initialValue): Returns [useCurrentValue, setState, { state, updateState, resetState, getSnapshot, initialState }].
  • createReducer(reducer, initialState): Returns [useCurrentValue, dispatch, { state, getSnapshot, initialState }].
  • createBooleanState(initialValue): Returns [useCurrentValue, { state, setTrue, setFalse, toggle, setState, updateState, resetState, getSnapshot, initialState }].

Bridges synstate observables with Preact Signals for fine-grained DOM updates without component re-renders. See the interactive demo to compare with the hooks approach.

  • toSignal(observable$): Convert an Observable (or InitializedObservable) into a Preact ReadonlySignal. The signal stays in sync with the observable. Can be embedded directly in JSX — only the affected text node updates, not the entire component.
  • toSignal(observable$, fallback): Same as above, with a fallback value for observables without an initial value.
  • fromSignal(signal): Convert a Preact Signal into a synstate InitializedSourceObservable. Returns [observable, dispose]. The observable mirrors the signal’s value, enabling the full synstate operator chain (map, debounce, switchMap, etc.).

Same as the hooks wrappers, but return a ReadonlySignal instead of a hook:

  • createState(initialValue): Returns [signal, setState, { state, updateState, resetState, getSnapshot, initialState }].
  • createReducer(reducer, initialState): Returns [signal, dispatch, { state, getSnapshot, initialState }].
  • createBooleanState(initialValue): Returns [signal, { state, setTrue, setFalse, toggle, setState, updateState, resetState, getSnapshot, initialState }].