@veams/status-quo
v1.1.0
Published
The manager to rule states in frontend.
Downloads
238
Keywords
Readme
@veams/status-quo
The manager to rule your state.
This page mirrors the demo content and adds a full API reference.
Table of Contents
- Overview
- Philosophy
- Demo
- Quickstart
- Handlers
- Hooks
- Singletons
- Composition
- Devtools
- Cleanup
- API Reference
- Migration
Overview
StatusQuo is a small, framework-agnostic state layer that focuses on explicit lifecycle, clear action APIs, and a minimal subscription surface. It ships two handler implementations with the same public interface: RxJS-backed observables and signals-backed stores.
Philosophy
- Swap the engine, keep the API. Your UI code stays the same when you switch from RxJS to Signals.
- Separate view and state. Handlers own transitions and expose actions; views subscribe to snapshots.
- Framework-agnostic core. Business logic lives outside the UI library; hooks provide the glue.
Demo
Live docs and demo:
https://veams.github.io/status-quo/
Quickstart
Install:
npm install @veams/status-quo rxjs @preact/signals-coreCreate a store and use it in a component:
import { ObservableStateHandler, useStateFactory } from '@veams/status-quo';
type CounterState = { count: number };
type CounterActions = {
increase: () => void;
decrease: () => void;
};
class CounterStore extends ObservableStateHandler<CounterState, CounterActions> {
constructor() {
super({ initialState: { count: 0 } });
}
getActions(): CounterActions {
return {
increase: () => this.setState({ count: this.getState().count + 1 }),
decrease: () => this.setState({ count: this.getState().count - 1 }),
};
}
}
const [state, actions] = useStateFactory(() => new CounterStore(), []);Handlers
StatusQuo provides two handler implementations with the same public interface:
ObservableStateHandler(RxJS-backed)SignalStateHandler(Signals-backed)
Both are built on BaseStateHandler, which provides the shared lifecycle and devtools support.
Hooks
useStateFactory(factory, deps)- Creates a handler instance per component and subscribes to its snapshot.
- Suitable for per-component or per-instance state.
useStateSingleton(singleton)- Uses a shared singleton handler across components.
Singletons
import { makeStateSingleton, useStateSingleton } from '@veams/status-quo';
const CounterSingleton = makeStateSingleton(() => new CounterStore());
const [state, actions] = useStateSingleton(CounterSingleton);Composition
Use only the slice you need. RxJS makes multi-source composition powerful and declarative with operators like combineLatest, switchMap, or debounceTime. Signals can derive values with computed and wire them into a parent store via bindSubscribable.
import { combineLatest } from 'rxjs';
// RxJS: combine handler streams (RxJS shines here)
class AppSignalStore extends SignalStateHandler<AppState, AppActions> {
private counter$ = CounterObservableStore.getInstance().getStateAsObservable();
private card$ = new CardObservableHandler();
constructor() {
super({ initialState: { counter: 0, cardTitle: '' }});
this.subscriptions.push(
combineLatest([
this.counter$,
this.card$,
]).subscribe(([counterState, cardState]) => {
this.setState({
counter: counterState,
cardTitle: cardState.title,
}, 'sync-combined');
})
)
}
}
// Signals: combine derived values via computed + bindSubscribable
import { computed } from '@preact/signals-core';
class AppSignalStore extends SignalStateHandler<AppState, AppActions> {
private counter = CounterSignalHandler.getInstance();
private card = new CardSignalHandler();
private combined$ = computed(() => ({
counter: this.counter.getSignal().value,
cardTitle: this.card.getSignal().value.title,
}));
constructor() {
super({ initialState: { counter: 0, cardTitle: '' }});
this.bindSubscribable(
{ subscribe: this.combined.subscribe.bind(this.combined), getSnapshot: () => this.combined.value },
(nextState) => this.setState(nextState, 'sync-combined')
);
}
}Devtools
Enable Redux Devtools integration with options.devTools:
class CounterStore extends ObservableStateHandler<CounterState, CounterActions> {
constructor() {
super({
initialState: { count: 0 },
options: { devTools: { enabled: true, namespace: 'Counter' } },
});
}
}Cleanup
Handlers expose subscribe, getSnapshot, and destroy for custom integrations:
const unsubscribe = store.subscribe(() => {
console.log(store.getSnapshot());
});
unsubscribe();
store.destroy();API Reference
StateSubscriptionHandler<V, A>
Required interface implemented by all handlers.
interface StateSubscriptionHandler<V, A> {
subscribe: (listener: () => void) => () => void;
getSnapshot: () => V;
destroy: () => void;
getInitialState: () => V;
getActions: () => A;
}BaseStateHandler<S, A>
Shared base class for all handlers.
Constructor:
protected constructor(initialState: S)Public methods:
getInitialState(): SgetState(): SgetSnapshot(): SsetState(next: Partial<S>, actionName = 'change'): voidsubscribe(listener: () => void): () => void(abstract)destroy(): voidgetActions(): A(abstract)
Protected helpers:
getStateValue(): S(abstract)setStateValue(next: S): void(abstract)initDevTools(options?: { enabled?: boolean; namespace: string }): voidbindSubscribable<T>(service: { subscribe: (listener: (value: T) => void) => () => void; getSnapshot?: () => T }, onChange: (value: T) => void): void- Registers the subscription on
this.subscriptionsand invokesonChangewith the current snapshot when available.
- Registers the subscription on
ObservableStateHandler<S, A>
RxJS-backed handler. Extends BaseStateHandler.
Constructor:
protected constructor({
initialState,
options
}: {
initialState: S;
options?: {
devTools?: { enabled?: boolean; namespace: string };
};
})Public methods:
getStateAsObservable(options?: { useDistinctUntilChanged?: boolean }): Observable<S>getStateItemAsObservable(key: keyof S): Observable<S[keyof S]>getObservableItem(key: keyof S): Observable<S[keyof S]>subscribe(listener: () => void): () => void
Notes:
- The observable stream uses
distinctUntilChangedby default (JSON compare). subscribedoes not fire for the initial value; it only fires on subsequent changes.
SignalStateHandler<S, A>
Signals-backed handler. Extends BaseStateHandler.
Constructor:
protected constructor({
initialState,
options
}: {
initialState: S;
options?: {
devTools?: { enabled?: boolean; namespace: string };
useDistinctUntilChanged?: boolean;
};
})Public methods:
getSignal(): Signal<S>subscribe(listener: () => void): () => void
Notes:
useDistinctUntilChangeddefaults totrue(JSON compare).
makeStateSingleton
function makeStateSingleton<S, A>(
factory: () => StateSubscriptionHandler<S, A>
): {
getInstance: () => StateSubscriptionHandler<S, A>;
}Hooks
useStateFactory<V, A, P extends unknown[]>(factory: (...args: P) => StateSubscriptionHandler<V, A>, params?: P)- Returns
[state, actions].
- Returns
useStateSingleton<V, A>(singleton: StateSingleton<V, A>)- Returns
[state, actions].
- Returns
Migration
From pre-1.0 releases:
- Rename
StateHandler->ObservableStateHandler. - Implement
subscribe()andgetSnapshot()on custom handlers. - Replace
getObservable()usage withsubscribe()in custom integrations. - Update devtools config:
- From:
super({ initialState, devTools: { ... } }) - To:
super({ initialState, options: { devTools: { ... } } })
- From:
