@veams/status-quo 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
9
9
  ### Added
10
10
  - `SignalStateHandler` (signals-backed state handler).
11
11
  - `BaseStateHandler` to share devtools and lifecycle APIs.
12
+ - `bindSubscribable` helper for managing external subscriptions.
12
13
  - Playground/demo with GitHub Pages deployment.
13
14
 
14
15
  ### Changed
package/README.md CHANGED
@@ -1,193 +1,312 @@
1
- # Status Quo (`@veams/status-quo`)
1
+ # @veams/status-quo
2
+ [![npm version](https://img.shields.io/npm/v/@veams/status-quo)](https://www.npmjs.com/package/@veams/status-quo)
2
3
 
3
- The `Manager` to rule your state.
4
+ <center>
5
+ <img src="assets/statusquo-logo.png" width="200" alt="StatusQuo Logo" style="margin: 0 auto;">
6
+ </center>
4
7
 
5
- ---
8
+ The manager to rule your state.
6
9
 
7
- ## Table of Content
10
+ This page mirrors the demo content and adds a full API reference.
8
11
 
9
- 1. [Getting Started](#getting-started)
10
- 2. [Example](#example)
12
+ ## Table of Contents
11
13
 
12
- ---
14
+ 1. [Overview](#overview)
15
+ 2. [Philosophy](#philosophy)
16
+ 3. [Demo](#demo)
17
+ 4. [Quickstart](#quickstart)
18
+ 5. [Handlers](#handlers)
19
+ 6. [Hooks](#hooks)
20
+ 7. [Singletons](#singletons)
21
+ 8. [Composition](#composition)
22
+ 9. [Devtools](#devtools)
23
+ 10. [Cleanup](#cleanup)
24
+ 11. [API Reference](#api-reference)
25
+ 12. [Migration](#migration)
13
26
 
14
- ## Getting Started
27
+ ## Overview
15
28
 
16
- 1. Create your own state handler which handles all the streams and a state you expose next to the actions
17
- 1. Use actions and state in your component
18
- 1. When using React, initialize the state handler with a custom hook called `useStateFactory()` (or `useStateSingleton()` for Singleton states)
29
+ 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.
19
30
 
31
+ ## Philosophy
20
32
 
21
- These three steps are necessary to create a completely decoupled state management solution without the need of creating custom hooks with `useEffect()`.
22
-
23
- __Note__:
24
- _Please keep in mind that dependencies for the hook needs to be flattened and cannot be used as an object due to how React works._
33
+ - Swap the engine, keep the API. Your UI code stays the same when you switch from RxJS to Signals.
34
+ - Separate view and state. Handlers own transitions and expose actions; views subscribe to snapshots.
35
+ - Framework-agnostic core. Business logic lives outside the UI library; hooks provide the glue.
25
36
 
26
37
  ## Demo
27
38
 
28
39
  Live docs and demo:
29
40
 
30
- ```
31
- https://veams.github.io/status-quo/
32
- ```
41
+ [https://veams.github.io/status-quo/](https://veams.github.io/status-quo/)
33
42
 
34
- ## Handlers
43
+ ## Quickstart
35
44
 
36
- Status Quo ships two handler implementations with the same public interface:
45
+ Install:
37
46
 
38
- - `ObservableStateHandler` (RxJS-backed)
39
- - `SignalStateHandler` (Signals-backed)
40
-
41
- ## Example
47
+ ```bash
48
+ npm install @veams/status-quo rxjs @preact/signals-core
49
+ ```
42
50
 
43
- Let's start with a simple state example.
44
- You should start with the abstract class `ObservableStateHandler`:
51
+ Create a store and use it in a component:
45
52
 
46
53
  ```ts
47
- import { useStateFactory, ObservableStateHandler } from '@veams/status-quo';
54
+ import { ObservableStateHandler, useStateFactory } from '@veams/status-quo';
55
+
56
+ type CounterState = { count: number };
48
57
 
49
- type CounterState = {
50
- count: number;
51
- };
52
58
  type CounterActions = {
53
59
  increase: () => void;
54
60
  decrease: () => void;
55
61
  };
56
62
 
57
- class CounterStateHandler extends ObservableStateHandler<CounterState, CounterActions> {
58
- constructor([startCount = 0]) {
59
- super({ initialState: { count: startCount } });
63
+ class CounterStore extends ObservableStateHandler<CounterState, CounterActions> {
64
+ constructor() {
65
+ super({ initialState: { count: 0 } });
60
66
  }
61
-
62
- getActions() {
67
+
68
+ getActions(): CounterActions {
63
69
  return {
64
- increase() {
65
- this.setState({
66
- count: this.getState() + 1
67
- })
68
- },
69
- decrease() {
70
- const currentState = this.getState();
71
-
72
- if (currentState.count > 0) {
73
- this.setState({
74
- count: currentState - 1
75
- })
76
- }
77
- }
78
- }
70
+ increase: () => this.setState({ count: this.getState().count + 1 }),
71
+ decrease: () => this.setState({ count: this.getState().count - 1 }),
72
+ };
79
73
  }
80
74
  }
81
75
 
82
- export function CounterStateFactory(...args) {
83
- return new CounterStateHandler(...args);
84
- }
76
+ const [state, actions] = useStateFactory(() => new CounterStore(), []);
85
77
  ```
86
78
 
87
- This can be used in our factory hook function:
88
-
89
- ```tsx
90
- import { useStateFactory } from '@veams/status-quo';
91
- import { CounterStateFactory } from './counter.state.js';
92
-
93
- const Counter = () => {
94
- const [state, actions] = useStateFactory(CounterStateFactory, [0]);
95
-
96
- return (
97
- <div>
98
- <h2>Counter: {state}</h2>
99
- <button onClick={actions.increase}>Increase</button>
100
- <button onClick={actions.decrease}>Decrease</button>
101
- </div>
102
- )
103
- }
104
- ```
79
+ ## Handlers
80
+
81
+ StatusQuo provides two handler implementations with the same public interface:
82
+
83
+ - `ObservableStateHandler` (RxJS-backed)
84
+ - `SignalStateHandler` (Signals-backed)
85
+
86
+ Both are built on `BaseStateHandler`, which provides the shared lifecycle and devtools support.
105
87
 
106
- **What about singletons?**
88
+ ## Hooks
107
89
 
108
- Therefore, you can use a simple singleton class or use `makeStateSingleton()` and pass it later on to the singleton hook function:
90
+ - `useStateFactory(factory, deps)`
91
+ - Creates a handler instance per component and subscribes to its snapshot.
92
+ - Suitable for per-component or per-instance state.
93
+ - `useStateSingleton(singleton)`
94
+ - Uses a shared singleton handler across components.
95
+
96
+ ## Singletons
109
97
 
110
98
  ```ts
111
- import { makeStateSingleton } from '@veams/status-quo';
99
+ import { makeStateSingleton, useStateSingleton } from '@veams/status-quo';
112
100
 
113
- import { CounterStateHandler } from './counter.state.js';
101
+ const CounterSingleton = makeStateSingleton(() => new CounterStore());
114
102
 
115
- export const CounterStateManager = makeStateSingleton(() => new CounterStateHandler([0]))
103
+ const [state, actions] = useStateSingleton(CounterSingleton);
116
104
  ```
117
105
 
118
- ```tsx
119
- import { useStateSingleton } from '@veams/status-quo';
120
- import { CounterStateManager } from './counter.singleton.js';
121
-
122
- const GlobalCounterHandler = () => {
123
- const [_, actions] = useStateSingleton(CounterStateManager);
124
-
125
- return (
126
- <div>
127
- <button onClick={actions.increase}>Increase</button>
128
- <button onClick={actions.decrease}>Decrease</button>
129
- </div>
130
- )
106
+ ## Composition
107
+
108
+ 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`.
109
+
110
+ ```ts
111
+ import { combineLatest } from 'rxjs';
112
+
113
+ // RxJS: combine handler streams (RxJS shines here)
114
+ class AppSignalStore extends SignalStateHandler<AppState, AppActions> {
115
+ private counter$ = CounterObservableStore.getInstance().getStateAsObservable();
116
+ private card$ = new CardObservableHandler();
117
+
118
+ constructor() {
119
+ super({ initialState: { counter: 0, cardTitle: '' }});
120
+
121
+ this.subscriptions.push(
122
+ combineLatest([
123
+ this.counter$,
124
+ this.card$,
125
+ ]).subscribe(([counterState, cardState]) => {
126
+ this.setState({
127
+ counter: counterState,
128
+ cardTitle: cardState.title,
129
+ }, 'sync-combined');
130
+ })
131
+ )
132
+ }
133
+
131
134
  }
132
135
 
133
- const GlobalCounterDisplay = () => {
134
- const [state] = useStateSingleton(CounterStateManager);
135
-
136
- return (
137
- <div>
138
- <h2>Counter: {state}</h2>
139
- </div>
140
- )
136
+ // Signals: combine derived values via computed + bindSubscribable
137
+ import { computed } from '@preact/signals-core';
138
+
139
+ class AppSignalStore extends SignalStateHandler<AppState, AppActions> {
140
+ private counter = CounterSignalHandler.getInstance();
141
+ private card = new CardSignalHandler();
142
+ private combined$ = computed(() => ({
143
+ counter: this.counter.getSignal().value,
144
+ cardTitle: this.card.getSignal().value.title,
145
+ }));
146
+
147
+ constructor() {
148
+ super({ initialState: { counter: 0, cardTitle: '' }});
149
+
150
+ this.bindSubscribable(
151
+ { subscribe: this.combined.subscribe.bind(this.combined), getSnapshot: () => this.combined.value },
152
+ (nextState) => this.setState(nextState, 'sync-combined')
153
+ );
154
+ }
141
155
  }
142
156
  ```
143
157
 
144
- ### What about debugging?
158
+ ## Devtools
145
159
 
146
- You know redux-devtools? You like it? We covered you (at least a bit)!
147
- You can enable the devtools in an easy way:
160
+ Enable Redux Devtools integration with `options.devTools`:
148
161
 
149
162
  ```ts
150
-
151
- class CounterStateHandler extends ObservableStateHandler<CounterState, CounterActions> {
152
- constructor([startCount = 0]) {
163
+ class CounterStore extends ObservableStateHandler<CounterState, CounterActions> {
164
+ constructor() {
153
165
  super({
154
- initialState: { count: startCount },
155
- options: {
156
- devTools: { enabled: true, namespace: 'Counter' },
157
- },
166
+ initialState: { count: 0 },
167
+ options: { devTools: { enabled: true, namespace: 'Counter' } },
158
168
  });
159
169
  }
170
+ }
171
+ ```
160
172
 
161
- getActions() {
162
- return {
163
- increase() {
164
- this.setState(
165
- {
166
- count: this.getState() + 1,
167
- },
168
- 'increase'
169
- );
170
- },
171
- decrease() {
172
- const currentState = this.getState();
173
-
174
- if (currentState.count > 0) {
175
- this.setState(
176
- {
177
- count: currentState - 1,
178
- },
179
- 'decrease'
180
- );
181
- }
182
- },
183
- };
184
- }
173
+ ## Cleanup
174
+
175
+ Handlers expose `subscribe`, `getSnapshot`, and `destroy` for custom integrations:
176
+
177
+ ```ts
178
+ const unsubscribe = store.subscribe(() => {
179
+ console.log(store.getSnapshot());
180
+ });
181
+
182
+ unsubscribe();
183
+ store.destroy();
184
+ ```
185
+
186
+ ## API Reference
187
+
188
+ ### `StateSubscriptionHandler<V, A>`
189
+
190
+ Required interface implemented by all handlers.
191
+
192
+ ```ts
193
+ interface StateSubscriptionHandler<V, A> {
194
+ subscribe: (listener: () => void) => () => void;
195
+ getSnapshot: () => V;
196
+ destroy: () => void;
197
+ getInitialState: () => V;
198
+ getActions: () => A;
185
199
  }
200
+ ```
201
+
202
+ ### `BaseStateHandler<S, A>`
203
+
204
+ Shared base class for all handlers.
205
+
206
+ Constructor:
207
+
208
+ ```ts
209
+ protected constructor(initialState: S)
210
+ ```
211
+
212
+ Public methods:
213
+
214
+ - `getInitialState(): S`
215
+ - `getState(): S`
216
+ - `getSnapshot(): S`
217
+ - `setState(next: Partial<S>, actionName = 'change'): void`
218
+ - `subscribe(listener: () => void): () => void` (abstract)
219
+ - `destroy(): void`
220
+ - `getActions(): A` (abstract)
221
+
222
+ Protected helpers:
223
+
224
+ - `getStateValue(): S` (abstract)
225
+ - `setStateValue(next: S): void` (abstract)
226
+ - `initDevTools(options?: { enabled?: boolean; namespace: string }): void`
227
+ - `bindSubscribable<T>(service: { subscribe: (listener: (value: T) => void) => () => void; getSnapshot?: () => T }, onChange: (value: T) => void): void`
228
+ - Registers the subscription on `this.subscriptions` and invokes `onChange` with the current snapshot when available.
229
+
230
+ ### `ObservableStateHandler<S, A>`
231
+
232
+ RxJS-backed handler. Extends `BaseStateHandler`.
233
+
234
+ Constructor:
186
235
 
187
- export function CounterStateFactory(...args) {
188
- return new CounterStateHandler(...args);
236
+ ```ts
237
+ protected constructor({
238
+ initialState,
239
+ options
240
+ }: {
241
+ initialState: S;
242
+ options?: {
243
+ devTools?: { enabled?: boolean; namespace: string };
244
+ };
245
+ })
246
+ ```
247
+
248
+ Public methods:
249
+
250
+ - `getStateAsObservable(options?: { useDistinctUntilChanged?: boolean }): Observable<S>`
251
+ - `getStateItemAsObservable(key: keyof S): Observable<S[keyof S]>`
252
+ - `getObservableItem(key: keyof S): Observable<S[keyof S]>`
253
+ - `subscribe(listener: () => void): () => void`
254
+
255
+ Notes:
256
+ - The observable stream uses `distinctUntilChanged` by default (JSON compare).
257
+ - `subscribe` does not fire for the initial value; it only fires on subsequent changes.
258
+
259
+ ### `SignalStateHandler<S, A>`
260
+
261
+ Signals-backed handler. Extends `BaseStateHandler`.
262
+
263
+ Constructor:
264
+
265
+ ```ts
266
+ protected constructor({
267
+ initialState,
268
+ options
269
+ }: {
270
+ initialState: S;
271
+ options?: {
272
+ devTools?: { enabled?: boolean; namespace: string };
273
+ useDistinctUntilChanged?: boolean;
274
+ };
275
+ })
276
+ ```
277
+
278
+ Public methods:
279
+
280
+ - `getSignal(): Signal<S>`
281
+ - `subscribe(listener: () => void): () => void`
282
+
283
+ Notes:
284
+ - `useDistinctUntilChanged` defaults to `true` (JSON compare).
285
+
286
+ ### `makeStateSingleton`
287
+
288
+ ```ts
289
+ function makeStateSingleton<S, A>(
290
+ factory: () => StateSubscriptionHandler<S, A>
291
+ ): {
292
+ getInstance: () => StateSubscriptionHandler<S, A>;
189
293
  }
190
294
  ```
191
295
 
192
- We just added the `options.devTools` option and also updated the `setState()` function by passing a second argument into it which is the actions name.
193
- Now you can open up the the browser extension and you are able to take a look at your actions and state(s).
296
+ ### Hooks
297
+
298
+ - `useStateFactory<V, A, P extends unknown[]>(factory: (...args: P) => StateSubscriptionHandler<V, A>, params?: P)`
299
+ - Returns `[state, actions]`.
300
+ - `useStateSingleton<V, A>(singleton: StateSingleton<V, A>)`
301
+ - Returns `[state, actions]`.
302
+
303
+ ## Migration
304
+
305
+ From pre-1.0 releases:
306
+
307
+ 1. Rename `StateHandler` -> `ObservableStateHandler`.
308
+ 2. Implement `subscribe()` and `getSnapshot()` on custom handlers.
309
+ 3. Replace `getObservable()` usage with `subscribe()` in custom integrations.
310
+ 4. Update devtools config:
311
+ - From: `super({ initialState, devTools: { ... } })`
312
+ - To: `super({ initialState, options: { devTools: { ... } } })`
Binary file
@@ -19,6 +19,10 @@ export declare abstract class BaseStateHandler<S, A> implements StateSubscriptio
19
19
  destroy(): void;
20
20
  protected abstract getStateValue(): S;
21
21
  protected abstract setStateValue(nextState: S): void;
22
+ protected bindSubscribable<T>(service: {
23
+ subscribe: (listener: (value: T) => void) => () => void;
24
+ getSnapshot?: () => T;
25
+ }, onChange: (value: T) => void): void;
22
26
  abstract subscribe(listener: () => void): () => void;
23
27
  abstract getActions(): A;
24
28
  private handleDevToolsEvents;
@@ -53,6 +53,12 @@ export class BaseStateHandler {
53
53
  destroy() {
54
54
  this.subscriptions.forEach((subscription) => subscription.unsubscribe());
55
55
  }
56
+ bindSubscribable(service, onChange) {
57
+ const unsubscribe = service.subscribe(onChange);
58
+ this.subscriptions = [...(this.subscriptions ?? []), { unsubscribe }];
59
+ if (service.getSnapshot)
60
+ onChange(service.getSnapshot());
61
+ }
56
62
  handleDevToolsEvents = (message) => {
57
63
  if (message.type !== 'DISPATCH') {
58
64
  return;
@@ -1 +1 @@
1
- {"version":3,"file":"base-state-handler.js","sourceRoot":"","sources":["../../src/store/base-state-handler.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"base-state-handler.js","sourceRoot":"","sources":["../../src/store/base-state-handler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAU9C,MAAM,sBAAsB,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC;AAEtE,MAAM,gBAAgB,GAAG;IACvB,KAAK,EAAE,IAAI;IACX,IAAI,EAAE,IAAI;IACV,OAAO,EAAE,KAAK;IACd,MAAM,EAAE,IAAI;IACZ,MAAM,EAAE,QAAQ;IAChB,IAAI,EAAE,IAAI;IACV,IAAI,EAAE,IAAI;IACV,OAAO,EAAE,IAAI;IACb,QAAQ,EAAE,KAAK;IACf,IAAI,EAAE,KAAK;CACZ,CAAC;AAEF,MAAM,OAAgB,gBAAgB;IACjB,YAAY,CAAI;IACzB,QAAQ,GAAoB,IAAI,CAAC;IAE3C,aAAa,GAAuC,EAAE,CAAC;IAEvD,YAAsB,YAAe;QACnC,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;IACnC,CAAC;IAES,YAAY,CAAC,eAAiC;QACtD,MAAM,aAAa,GAAG;YACpB,GAAG,sBAAsB;YACzB,GAAG,eAAe;SACnB,CAAC;QAEF,IAAI,CAAC,aAAa,CAAC,OAAO,EAAE,CAAC;YAC3B,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;YACrB,OAAO;QACT,CAAC;QAED,IAAI,CAAC,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,YAAY,EAAE;YAC9C,IAAI,EAAE,aAAa,CAAC,SAAS;YAC7B,UAAU,EAAE,aAAa,CAAC,SAAS,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC;YACtE,cAAc,EAAE,IAAI,CAAC,UAAU,EAAE;YACjC,QAAQ,EAAE,gBAAgB;SAC3B,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;IACtD,CAAC;IAED,eAAe;QACb,OAAO,IAAI,CAAC,YAAY,CAAC;IAC3B,CAAC;IAED,QAAQ;QACN,OAAO,IAAI,CAAC,aAAa,EAAE,CAAC;IAC9B,CAAC;IAED,WAAW;QACT,OAAO,IAAI,CAAC,QAAQ,EAAE,CAAC;IACzB,CAAC;IAED,QAAQ,CAAC,QAAoB,EAAE,UAAU,GAAG,QAAQ;QAClD,MAAM,SAAS,GAAG,EAAE,GAAG,IAAI,CAAC,QAAQ,EAAE,EAAE,GAAG,QAAQ,EAAE,CAAC;QACtD,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC;QAC9B,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;IAC7C,CAAC;IAED,OAAO;QACL,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,YAAY,EAAE,EAAE,CAAC,YAAY,CAAC,WAAW,EAAE,CAAC,CAAC;IAC3E,CAAC;IAIS,gBAAgB,CACxB,OAA2F,EAC3F,QAA4B;QAE5B,MAAM,WAAW,GAAG,OAAO,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QAChD,IAAI,CAAC,aAAa,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,aAAa,IAAI,EAAE,CAAC,EAAE,EAAE,WAAW,EAAE,CAAC,CAAC;QAEtE,IAAI,OAAO,CAAC,WAAW;YAAE,QAAQ,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAC3D,CAAC;IAKO,oBAAoB,GAAG,CAAC,OAAuB,EAAE,EAAE;QACzD,IAAI,OAAO,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YAChC,OAAO;QACT,CAAC;QAED,QAAQ,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;YAC7B,KAAK,OAAO;gBACV,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC,CAAC;gBAC3C,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC,CAAC;gBAC5C,MAAM;YAER,KAAK,QAAQ;gBACX,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;gBACpC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;gBACrC,MAAM;YAER,KAAK,eAAe,CAAC;YACrB,KAAK,gBAAgB;gBACnB,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;gBAC9C,MAAM;YAER;gBACE,MAAM;QACV,CAAC;IACH,CAAC,CAAC;CACH"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veams/status-quo",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "The manager to rule states in frontend.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -16,6 +16,7 @@ import 'prismjs/components/prism-bash';
16
16
  import philosophySwap from './assets/philosophy-swap.svg';
17
17
  import philosophySeparation from './assets/philosophy-separation.svg';
18
18
  import philosophyAgnostic from './assets/philosophy-agnostic.svg';
19
+ import statusQuoLogo from './assets/statusquo-logo.png';
19
20
 
20
21
  type CounterState = {
21
22
  count: number;
@@ -227,48 +228,49 @@ const CounterSingleton = makeStateSingleton(() => new CounterStore());
227
228
 
228
229
  const [state, actions] = useStateSingleton(CounterSingleton);`;
229
230
 
230
- const composeSnippet = `// Observable: subscribe to a single field
231
- class UserStore extends ObservableStateHandler<UserState, UserActions> { /* ... */ }
232
- class CartStore extends ObservableStateHandler<CartState, CartActions> { /* ... */ }
231
+ const composeSnippet = `import { combineLatest } from "rxjs";
233
232
 
234
- class AppStore extends ObservableStateHandler<AppState, AppActions> {
235
- private user = new UserStore();
236
- private cart = new CartStore();
233
+ // RxJS: combine handler streams (RxJS shines here)
234
+ class AppSignalStore extends SignalStateHandler<AppState, AppActions> {
235
+ private counter$ = CounterObservableStore.getInstance().getStateAsObservable();
236
+ private card$ = new CardObservableHandler();
237
237
 
238
238
  constructor() {
239
- super({ initialState: { status: this.user.getState().status, cart: this.cart.getState() } });
240
-
241
- this.subscriptions = [
242
- { unsubscribe: this.user.getStateItemAsObservable('status').subscribe(() => this.sync()) },
243
- { unsubscribe: this.cart.subscribe(() => this.syncCart()) }
244
- ];
245
- }
246
-
247
- private sync() {
248
- this.setState({ status: this.user.getState().status }, 'sync-status');
239
+ super({ initialState: { counter: 0, cardTitle: "" }});
240
+
241
+ this.subscriptions.push(
242
+ combineLatest([
243
+ this.counter$,
244
+ this.card$,
245
+ ]).subscribe(([counterState, cardState]) => {
246
+ this.setState({
247
+ counter: counterState,
248
+ cardTitle: cardState.title,
249
+ }, "sync-combined");
250
+ })
251
+ )
249
252
  }
250
253
 
251
- private syncCart() {
252
- this.setState({ cart: this.cart.getState() }, 'sync-cart');
253
- }
254
254
  }
255
255
 
256
- // Signal: derive a single field with computed()
257
- import { computed } from '@preact/signals-core';
258
-
259
- class UserSignalStore extends SignalStateHandler<UserState, UserActions> { /* ... */ }
256
+ // Signals: combine derived values via computed + bindSubscribable
257
+ import { computed } from "@preact/signals-core";
260
258
 
261
259
  class AppSignalStore extends SignalStateHandler<AppState, AppActions> {
262
- private user = new UserSignalStore();
263
- private status = computed(() => this.user.getState().status);
260
+ private counter = CounterSignalHandler.getInstance().getSignal();
261
+ private card = new CardSignalHandler();
262
+ private combined = computed(() => ({
263
+ counter: this.counter.getSignal().value,
264
+ cardTitle: this.card.getSignal().value.title,
265
+ }));
264
266
 
265
267
  constructor() {
266
- super({ initialState: { status: this.user.getState().status } });
267
- this.subscriptions = [{ unsubscribe: this.user.subscribe(() => this.sync()) }];
268
- }
268
+ super({ initialState: { counter: 0, cardTitle: "" }});
269
269
 
270
- private sync() {
271
- this.setState({ status: this.status.value }, 'sync-status');
270
+ this.bindSubscribable(
271
+ { subscribe: this.combined.subscribe.bind(this.combined), getSnapshot: () => this.combined.value },
272
+ (nextState) => this.setState(nextState, "sync-combined")
273
+ );
272
274
  }
273
275
  }`;
274
276
 
@@ -279,8 +281,7 @@ class AppSignalStore extends SignalStateHandler<AppState, AppActions> {
279
281
  return (
280
282
  <div className="app">
281
283
  <div className="brand-bar">
282
- <span className="brand-dot" />
283
- <span>Status Quo Demo</span>
284
+ <img src={statusQuoLogo} alt="StatusQuo logo" className="brand-logo" />
284
285
  </div>
285
286
  <nav className="nav">
286
287
  <div className="nav-links">
@@ -300,7 +301,7 @@ class AppSignalStore extends SignalStateHandler<AppState, AppActions> {
300
301
  <p className="eyebrow">Philosophy</p>
301
302
  <h1>State management that stays out of your way</h1>
302
303
  <p className="subtext">
303
- Status Quo treats state handlers as small, composable objects with explicit
304
+ <span>StatusQuo</span> treats state handlers as small, composable objects with explicit
304
305
  lifecycle and a tiny interface. Components subscribe to snapshots, not
305
306
  framework‑specific store APIs. That makes it easy to swap the engine under the
306
307
  hood—RxJS for observable streams or Preact Signals for ultra‑light reactive state.
@@ -433,9 +434,10 @@ class AppSignalStore extends SignalStateHandler<AppState, AppActions> {
433
434
  <p className="eyebrow">Compose</p>
434
435
  <h2>Combine multiple handlers</h2>
435
436
  <p>
436
- Use only the slice you need. Observables can subscribe to a single key (or use
437
- `combineLatest`), while Signals can derive values with `computed`. This keeps
438
- parent stores lean and focused.
437
+ Use only the slice you need. RxJS makes multi-source composition powerful and
438
+ declarative with operators like `combineLatest`, `switchMap`, or `debounceTime`.
439
+ Signals can derive values with `computed` and wire them into a parent store via
440
+ `bindSubscribable`. This keeps parent stores lean and focused.
439
441
  </p>
440
442
  </div>
441
443
  <pre className="code-block">
@@ -54,12 +54,10 @@ body {
54
54
  width: 100%;
55
55
  }
56
56
 
57
- .brand-dot {
58
- width: 12px;
59
- height: 12px;
60
- border-radius: 50%;
61
- background: var(--accent);
62
- box-shadow: 0 0 0 4px rgba(47, 107, 255, 0.2);
57
+ .brand-logo {
58
+ width: 260px;
59
+ height: auto;
60
+ display: block;
63
61
  }
64
62
 
65
63
  .nav {
@@ -130,6 +128,12 @@ p {
130
128
  margin: 0;
131
129
  color: var(--muted);
132
130
  line-height: 1.6;
131
+
132
+ span {
133
+ font-family: monospace;
134
+ font-weight: 700;
135
+ font-size: .9rem;
136
+ }
133
137
  }
134
138
 
135
139
  .muted {
@@ -78,6 +78,15 @@ export abstract class BaseStateHandler<S, A> implements StateSubscriptionHandler
78
78
 
79
79
  protected abstract getStateValue(): S;
80
80
  protected abstract setStateValue(nextState: S): void;
81
+ protected bindSubscribable<T>(
82
+ service: { subscribe: (listener: (value: T) => void) => () => void; getSnapshot?: () => T },
83
+ onChange: (value: T) => void
84
+ ) {
85
+ const unsubscribe = service.subscribe(onChange);
86
+ this.subscriptions = [...(this.subscriptions ?? []), { unsubscribe }];
87
+
88
+ if (service.getSnapshot) onChange(service.getSnapshot());
89
+ }
81
90
 
82
91
  abstract subscribe(listener: () => void): () => void;
83
92
  abstract getActions(): A;