@veams/status-quo 1.0.0 → 1.2.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.
Files changed (68) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +420 -130
  3. package/assets/statusquo-logo.png +0 -0
  4. package/dist/hooks/__tests__/state-selector.spec.d.ts +4 -0
  5. package/dist/hooks/__tests__/state-selector.spec.js +384 -0
  6. package/dist/hooks/__tests__/state-selector.spec.js.map +1 -0
  7. package/dist/hooks/__tests__/state-singleton.spec.d.ts +4 -0
  8. package/dist/hooks/__tests__/state-singleton.spec.js +97 -0
  9. package/dist/hooks/__tests__/state-singleton.spec.js.map +1 -0
  10. package/dist/hooks/index.d.ts +3 -0
  11. package/dist/hooks/index.js +3 -0
  12. package/dist/hooks/index.js.map +1 -1
  13. package/dist/hooks/state-actions.d.ts +2 -0
  14. package/dist/hooks/state-actions.js +5 -0
  15. package/dist/hooks/state-actions.js.map +1 -0
  16. package/dist/hooks/state-factory.d.ts +5 -0
  17. package/dist/hooks/state-factory.js +10 -6
  18. package/dist/hooks/state-factory.js.map +1 -1
  19. package/dist/hooks/state-handler.d.ts +2 -0
  20. package/dist/hooks/state-handler.js +9 -0
  21. package/dist/hooks/state-handler.js.map +1 -0
  22. package/dist/hooks/state-singleton.d.ts +4 -0
  23. package/dist/hooks/state-singleton.js +3 -5
  24. package/dist/hooks/state-singleton.js.map +1 -1
  25. package/dist/hooks/state-subscription-selector.d.ts +5 -0
  26. package/dist/hooks/state-subscription-selector.js +29 -0
  27. package/dist/hooks/state-subscription-selector.js.map +1 -0
  28. package/dist/hooks/state-subscription.d.ts +8 -1
  29. package/dist/hooks/state-subscription.js +49 -10
  30. package/dist/hooks/state-subscription.js.map +1 -1
  31. package/dist/index.d.ts +4 -4
  32. package/dist/index.js +2 -2
  33. package/dist/index.js.map +1 -1
  34. package/dist/store/__tests__/observable-state-handler.spec.js +4 -0
  35. package/dist/store/__tests__/observable-state-handler.spec.js.map +1 -1
  36. package/dist/store/base-state-handler.d.ts +4 -0
  37. package/dist/store/base-state-handler.js +6 -0
  38. package/dist/store/base-state-handler.js.map +1 -1
  39. package/dist/store/index.d.ts +1 -1
  40. package/dist/store/observable-state-handler.d.ts +2 -0
  41. package/dist/store/observable-state-handler.js +5 -1
  42. package/dist/store/observable-state-handler.js.map +1 -1
  43. package/dist/store/state-singleton.d.ts +4 -1
  44. package/dist/store/state-singleton.js +11 -2
  45. package/dist/store/state-singleton.js.map +1 -1
  46. package/docs/assets/index-Ci4A1zSh.js +187 -0
  47. package/docs/assets/index-WFTLEHd3.css +1 -0
  48. package/docs/assets/statusquo-logo-8GVRbxpc.png +0 -0
  49. package/docs/index.html +13 -0
  50. package/package.json +1 -1
  51. package/playground/src/App.tsx +284 -81
  52. package/playground/src/assets/statusquo-logo.png +0 -0
  53. package/playground/src/styles.css +137 -6
  54. package/src/hooks/__tests__/state-selector.spec.tsx +607 -0
  55. package/src/hooks/__tests__/state-singleton.spec.tsx +151 -0
  56. package/src/hooks/index.ts +3 -0
  57. package/src/hooks/state-actions.tsx +7 -0
  58. package/src/hooks/state-factory.tsx +32 -6
  59. package/src/hooks/state-handler.tsx +16 -0
  60. package/src/hooks/state-singleton.tsx +17 -7
  61. package/src/hooks/state-subscription-selector.tsx +70 -0
  62. package/src/hooks/state-subscription.tsx +98 -21
  63. package/src/index.ts +12 -3
  64. package/src/store/__tests__/observable-state-handler.spec.ts +6 -0
  65. package/src/store/base-state-handler.ts +9 -0
  66. package/src/store/index.ts +1 -1
  67. package/src/store/observable-state-handler.ts +6 -1
  68. package/src/store/state-singleton.ts +21 -3
package/CHANGELOG.md CHANGED
@@ -4,11 +4,55 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
+ ## [1.2.0] - 2026-02-25
8
+
9
+ ### Added
10
+ - New composable hook APIs:
11
+ - `useStateHandler(factory, params?)`
12
+ - `useStateActions(handler)`
13
+ - `useStateSubscription(source, selector?, isEqual?)`
14
+ - Selector + equality support across shortcut hooks:
15
+ - `useStateFactory(factory, selector?, isEqual?, params?)`
16
+ - `useStateSingleton(singleton, selector?, isEqual?)`
17
+ - `StateSingletonOptions` with `destroyOnNoConsumers?: boolean` (default: `true`).
18
+ - Ref-counted singleton lifecycle handling so shared singleton instances are only destroyed when the last consumer unmounts.
19
+ - New hook test coverage for:
20
+ - composed API usage (`useStateHandler + useStateActions + useStateSubscription`)
21
+ - selector subscriptions with and without custom equality
22
+ - singleton subscription behavior and `destroyOnNoConsumers: false`
23
+ - full-snapshot subscription behavior.
24
+
25
+ ### Changed
26
+ - `useStateFactory` now composes `useStateHandler` + `useStateSubscription` internally.
27
+ - `useStateSubscription` now:
28
+ - accepts either `StateSubscriptionHandler` or `StateSingleton`
29
+ - returns `[selectedState, actions]`
30
+ - supports selector/equality without requiring separate selector-specific hook APIs.
31
+ - `useStateSingleton` is now a shortcut over `useStateSubscription(singleton, selector?, isEqual?)`.
32
+ - `makeStateSingleton` now accepts options and manages instance destruction through explicit lifecycle controls.
33
+ - Public exports extended:
34
+ - Added `useStateHandler`, `useStateActions`, `useStateSubscription`
35
+ - Added exported `StateSingletonOptions` type.
36
+ - Observable handler naming aligned with signal convention:
37
+ - Added `getObservable(key)` as the canonical API.
38
+
39
+ ### Deprecated
40
+ - `ObservableStateHandler#getObservableItem(key)` is now deprecated in favor of `getObservable(key)`.
41
+
42
+ ### Documentation
43
+ - README rewritten with a dedicated API guide that documents base composition, shortcut composition, singleton lifecycle options, and usage examples.
44
+ - Playground documentation significantly expanded:
45
+ - dedicated API section grouped by base composition, shortcut composition, and helper functions
46
+ - clearer singleton lifecycle explanation and examples
47
+ - improved card hierarchy and spacing for readability
48
+ - responsive/toggleable navigation for better mobile usage.
49
+
7
50
  ## [1.0.0] - 2026-02-17
8
51
 
9
52
  ### Added
10
53
  - `SignalStateHandler` (signals-backed state handler).
11
54
  - `BaseStateHandler` to share devtools and lifecycle APIs.
55
+ - `bindSubscribable` helper for managing external subscriptions.
12
56
  - Playground/demo with GitHub Pages deployment.
13
57
 
14
58
  ### Changed
@@ -26,4 +70,5 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
26
70
  - From: `super({ initialState, devTools: { ... } })`
27
71
  - To: `super({ initialState, options: { devTools: { ... } } })`
28
72
 
73
+ [1.2.0]: https://github.com/Veams/status-quo/compare/v1.0.0...v1.2.0
29
74
  [1.0.0]: https://github.com/Veams/status-quo/releases/tag/v1.0.0
package/README.md CHANGED
@@ -1,193 +1,483 @@
1
- # Status Quo (`@veams/status-quo`)
1
+ <center>
2
+ <img src="assets/statusquo-logo.png" width="200" alt="StatusQuo Logo" style="margin: 0 auto;">
3
+ </center>
2
4
 
3
- The `Manager` to rule your state.
5
+ # @veams/status-quo
6
+ [![npm version](https://img.shields.io/npm/v/@veams/status-quo)](https://www.npmjs.com/package/@veams/status-quo)
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. [API Guide](#api-guide)
23
+ 10. [Devtools](#devtools)
24
+ 11. [Cleanup](#cleanup)
25
+ 12. [API Reference](#api-reference)
26
+ 13. [Migration](#migration)
13
27
 
14
- ## Getting Started
28
+ ## Overview
15
29
 
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)
30
+ 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
31
 
32
+ ## Philosophy
20
33
 
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._
34
+ - Swap the engine, keep the API. Your UI code stays the same when you switch from RxJS to Signals.
35
+ - Separate view and state. Handlers own transitions and expose actions; views subscribe to snapshots.
36
+ - Framework-agnostic core. Business logic lives outside the UI library; hooks provide the glue.
25
37
 
26
38
  ## Demo
27
39
 
28
40
  Live docs and demo:
29
41
 
30
- ```
31
- https://veams.github.io/status-quo/
32
- ```
33
-
34
- ## Handlers
42
+ [https://veams.github.io/status-quo/](https://veams.github.io/status-quo/)
35
43
 
36
- Status Quo ships two handler implementations with the same public interface:
44
+ ## Quickstart
37
45
 
38
- - `ObservableStateHandler` (RxJS-backed)
39
- - `SignalStateHandler` (Signals-backed)
46
+ Install:
40
47
 
41
- ## Example
48
+ ```bash
49
+ npm install @veams/status-quo rxjs @preact/signals-core
50
+ ```
42
51
 
43
- Let's start with a simple state example.
44
- You should start with the abstract class `ObservableStateHandler`:
52
+ Create a store and use it in a component:
45
53
 
46
54
  ```ts
47
- import { useStateFactory, ObservableStateHandler } from '@veams/status-quo';
55
+ import { ObservableStateHandler, useStateFactory } from '@veams/status-quo';
56
+
57
+ type CounterState = { count: number };
48
58
 
49
- type CounterState = {
50
- count: number;
51
- };
52
59
  type CounterActions = {
53
60
  increase: () => void;
54
61
  decrease: () => void;
55
62
  };
56
63
 
57
- class CounterStateHandler extends ObservableStateHandler<CounterState, CounterActions> {
58
- constructor([startCount = 0]) {
59
- super({ initialState: { count: startCount } });
64
+ class CounterStore extends ObservableStateHandler<CounterState, CounterActions> {
65
+ constructor() {
66
+ super({ initialState: { count: 0 } });
60
67
  }
61
-
62
- getActions() {
68
+
69
+ getActions(): CounterActions {
63
70
  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
- }
71
+ increase: () => this.setState({ count: this.getState().count + 1 }),
72
+ decrease: () => this.setState({ count: this.getState().count - 1 }),
73
+ };
79
74
  }
80
75
  }
81
76
 
82
- export function CounterStateFactory(...args) {
83
- return new CounterStateHandler(...args);
84
- }
77
+ const [state, actions] = useStateFactory(() => new CounterStore(), []);
85
78
  ```
86
79
 
87
- This can be used in our factory hook function:
80
+ ## Handlers
88
81
 
89
- ```tsx
90
- import { useStateFactory } from '@veams/status-quo';
91
- import { CounterStateFactory } from './counter.state.js';
82
+ StatusQuo provides two handler implementations with the same public interface:
92
83
 
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
- }
84
+ - `ObservableStateHandler` (RxJS-backed)
85
+ - `SignalStateHandler` (Signals-backed)
86
+
87
+ Both are built on `BaseStateHandler`, which provides the shared lifecycle and devtools support.
88
+
89
+ ## Hooks
90
+
91
+ Use `useStateHandler + useStateActions + useStateSubscription` as the base composition.
92
+ `useStateFactory` and `useStateSingleton` are shortcut APIs over that composition.
93
+ For full signatures and practical examples, see [API Guide](#api-guide).
94
+
95
+ - `useStateHandler(factory, params)`
96
+ - Creates and memoizes one handler instance per component.
97
+ - `useStateActions(handler)`
98
+ - Returns actions without subscribing to state.
99
+ - `useStateSubscription(handlerOrSingleton, selector?, isEqual?)`
100
+ - Subscribes to full state or a selected slice and returns `[state, actions]`.
101
+ - `useStateFactory(factory, selector?, isEqual?, params?)`
102
+ - Shortcut for `useStateHandler + useStateSubscription`.
103
+ - `useStateSingleton(singleton, selector?, isEqual?)`
104
+ - Shortcut for `useStateSubscription(singleton, selector?, isEqual?)`.
105
+
106
+ Recommended composition:
107
+
108
+ ```ts
109
+ const handler = useStateHandler(createUserStore, []);
110
+ const actions = useStateActions(handler);
111
+ const [name] = useStateSubscription(handler, (state) => state.user.name);
112
+
113
+ const [singletonName] = useStateSubscription(UserSingleton, (state) => state.user.name);
104
114
  ```
105
115
 
106
- **What about singletons?**
116
+ ## Singletons
107
117
 
108
- Therefore, you can use a simple singleton class or use `makeStateSingleton()` and pass it later on to the singleton hook function:
118
+ Use singletons for shared state across multiple components.
109
119
 
110
120
  ```ts
111
- import { makeStateSingleton } from '@veams/status-quo';
121
+ import { makeStateSingleton, useStateSingleton } from '@veams/status-quo';
122
+
123
+ // Default behavior: singleton is destroyed when the last consumer unmounts.
124
+ const CounterSingleton = makeStateSingleton(() => new CounterStore());
125
+
126
+ const [state, actions] = useStateSingleton(CounterSingleton);
127
+ ```
112
128
 
113
- import { CounterStateHandler } from './counter.state.js';
129
+ Keep a singleton instance alive across unmounts:
114
130
 
115
- export const CounterStateManager = makeStateSingleton(() => new CounterStateHandler([0]))
131
+ ```ts
132
+ const PersistentCounterSingleton = makeStateSingleton(() => new CounterStore(), {
133
+ destroyOnNoConsumers: false,
134
+ });
116
135
  ```
117
136
 
118
- ```tsx
119
- import { useStateSingleton } from '@veams/status-quo';
120
- import { CounterStateManager } from './counter.singleton.js';
137
+ Use this for app-level stores that should survive route/component unmounts. Keep the default for stores that should release resources when unused.
138
+
139
+ ## Composition
140
+
141
+ 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`.
142
+
143
+ ```ts
144
+ import { combineLatest } from 'rxjs';
145
+
146
+ // RxJS: combine handler streams (RxJS shines here)
147
+ class AppSignalStore extends SignalStateHandler<AppState, AppActions> {
148
+ private counter$ = CounterObservableStore.getInstance().getStateAsObservable();
149
+ private card$ = new CardObservableHandler();
150
+
151
+ constructor() {
152
+ super({ initialState: { counter: 0, cardTitle: '' }});
153
+
154
+ this.subscriptions.push(
155
+ combineLatest([
156
+ this.counter$,
157
+ this.card$,
158
+ ]).subscribe(([counterState, cardState]) => {
159
+ this.setState({
160
+ counter: counterState,
161
+ cardTitle: cardState.title,
162
+ }, 'sync-combined');
163
+ })
164
+ )
165
+ }
121
166
 
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
- )
131
167
  }
132
168
 
133
- const GlobalCounterDisplay = () => {
134
- const [state] = useStateSingleton(CounterStateManager);
135
-
136
- return (
137
- <div>
138
- <h2>Counter: {state}</h2>
139
- </div>
140
- )
169
+ // Signals: combine derived values via computed + bindSubscribable
170
+ import { computed } from '@preact/signals-core';
171
+
172
+ class AppSignalStore extends SignalStateHandler<AppState, AppActions> {
173
+ private counter = CounterSignalHandler.getInstance();
174
+ private card = new CardSignalHandler();
175
+ private combined$ = computed(() => ({
176
+ counter: this.counter.getSignal().value,
177
+ cardTitle: this.card.getSignal().value.title,
178
+ }));
179
+
180
+ constructor() {
181
+ super({ initialState: { counter: 0, cardTitle: '' }});
182
+
183
+ this.bindSubscribable(
184
+ { subscribe: this.combined.subscribe.bind(this.combined), getSnapshot: () => this.combined.value },
185
+ (nextState) => this.setState(nextState, 'sync-combined')
186
+ );
187
+ }
141
188
  }
142
189
  ```
143
190
 
144
- ### What about debugging?
191
+ ## API Guide
192
+
193
+ This section documents the primary public API with behavior notes and usage examples.
194
+
195
+ ### `useStateHandler(factory, params?)`
196
+
197
+ Creates one handler instance per component mount and returns it.
198
+
199
+ - `factory`: function returning a `StateSubscriptionHandler`
200
+ - `params`: optional factory params tuple
201
+ - lifecycle note: params are applied when the handler instance is created for that mount
202
+
203
+ ```ts
204
+ const handler = useStateHandler(createUserStore, []);
205
+ ```
206
+
207
+ ### `useStateActions(handler)`
208
+
209
+ Returns actions from a handler without subscribing to state changes.
210
+ Use this in action-only components to avoid rerenders from state updates.
211
+
212
+ ```ts
213
+ const handler = useStateHandler(createUserStore, []);
214
+ const actions = useStateActions(handler);
215
+ ```
216
+
217
+ ### `useStateSubscription(source, selector?, isEqual?)`
218
+
219
+ Subscribes to either a handler instance or a singleton and returns `[selectedState, actions]`.
220
+
221
+ - `source`: `StateSubscriptionHandler` or `StateSingleton`
222
+ - `selector`: optional projection function; defaults to identity
223
+ - `isEqual`: optional equality function; defaults to `Object.is`
224
+
225
+ Full snapshot subscription:
226
+
227
+ ```ts
228
+ const handler = useStateHandler(createUserStore, []);
229
+ const [state, actions] = useStateSubscription(handler);
230
+ ```
231
+
232
+ Selector subscription:
233
+
234
+ ```ts
235
+ const [name, actions] = useStateSubscription(
236
+ handler,
237
+ (state) => state.user.name
238
+ );
239
+ ```
240
+
241
+ Selector with custom equality:
242
+
243
+ ```ts
244
+ const [profile] = useStateSubscription(
245
+ handler,
246
+ (state) => state.user.profile,
247
+ (current, next) => current.id === next.id && current.role === next.role
248
+ );
249
+ ```
250
+
251
+ Singleton source:
252
+
253
+ ```ts
254
+ const [session, actions] = useStateSubscription(SessionSingleton);
255
+ ```
256
+
257
+ Lifecycle note for singleton sources:
258
+ - Consumers are ref-counted.
259
+ - The singleton instance is only destroyed when the last consumer unmounts and `destroyOnNoConsumers !== false`.
260
+
261
+ ### `useStateFactory(factory, selector?, isEqual?, params?)`
262
+
263
+ Shortcut API for `useStateHandler + useStateSubscription`.
264
+
265
+ - `useStateFactory(factory, params)`
266
+ - `useStateFactory(factory, selector, params)`
267
+ - `useStateFactory(factory, selector, isEqual, params)`
268
+
269
+ ```ts
270
+ const [state, actions] = useStateFactory(createUserStore, []);
271
+ const [name] = useStateFactory(createUserStore, (state) => state.user.name, []);
272
+ const [profile] = useStateFactory(
273
+ createUserStore,
274
+ (state) => state.user.profile,
275
+ (current, next) => current.id === next.id,
276
+ []
277
+ );
278
+ ```
279
+
280
+ ### `makeStateSingleton(factory, options?)`
145
281
 
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:
282
+ Creates a shared singleton provider for a handler instance.
148
283
 
149
284
  ```ts
285
+ const UserSingleton = makeStateSingleton(() => new UserStore());
286
+ ```
287
+
288
+ Options:
289
+
290
+ ```ts
291
+ type StateSingletonOptions = {
292
+ destroyOnNoConsumers?: boolean; // default: true
293
+ };
294
+ ```
295
+
296
+ - `true` (default): destroy instance after last consumer unmounts
297
+ - `false`: keep instance alive across periods with zero consumers
298
+
299
+ ```ts
300
+ const PersistentUserSingleton = makeStateSingleton(() => new UserStore(), {
301
+ destroyOnNoConsumers: false,
302
+ });
303
+ ```
304
+
305
+ ### `useStateSingleton(singleton, selector?, isEqual?)`
150
306
 
151
- class CounterStateHandler extends ObservableStateHandler<CounterState, CounterActions> {
152
- constructor([startCount = 0]) {
307
+ Shortcut API for `useStateSubscription(singleton, selector?, isEqual?)`.
308
+
309
+ ```ts
310
+ const [state, actions] = useStateSingleton(UserSingleton);
311
+ const [name] = useStateSingleton(UserSingleton, (state) => state.user.name);
312
+ ```
313
+
314
+ ## Devtools
315
+
316
+ Enable Redux Devtools integration with `options.devTools`:
317
+
318
+ ```ts
319
+ class CounterStore extends ObservableStateHandler<CounterState, CounterActions> {
320
+ constructor() {
153
321
  super({
154
- initialState: { count: startCount },
155
- options: {
156
- devTools: { enabled: true, namespace: 'Counter' },
157
- },
322
+ initialState: { count: 0 },
323
+ options: { devTools: { enabled: true, namespace: 'Counter' } },
158
324
  });
159
325
  }
326
+ }
327
+ ```
160
328
 
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
- }
329
+ ## Cleanup
330
+
331
+ Handlers expose `subscribe`, `getSnapshot`, and `destroy` for custom integrations:
332
+
333
+ ```ts
334
+ const unsubscribe = store.subscribe(() => {
335
+ console.log(store.getSnapshot());
336
+ });
337
+
338
+ unsubscribe();
339
+ store.destroy();
340
+ ```
341
+
342
+ ## API Reference
343
+
344
+ ### `StateSubscriptionHandler<V, A>`
345
+
346
+ Required interface implemented by all handlers.
347
+
348
+ ```ts
349
+ interface StateSubscriptionHandler<V, A> {
350
+ subscribe: (listener: () => void) => () => void;
351
+ getSnapshot: () => V;
352
+ destroy: () => void;
353
+ getInitialState: () => V;
354
+ getActions: () => A;
185
355
  }
356
+ ```
357
+
358
+ ### `BaseStateHandler<S, A>`
359
+
360
+ Shared base class for all handlers.
361
+
362
+ Constructor:
363
+
364
+ ```ts
365
+ protected constructor(initialState: S)
366
+ ```
367
+
368
+ Public methods:
369
+
370
+ - `getInitialState(): S`
371
+ - `getState(): S`
372
+ - `getSnapshot(): S`
373
+ - `setState(next: Partial<S>, actionName = 'change'): void`
374
+ - `subscribe(listener: () => void): () => void` (abstract)
375
+ - `destroy(): void`
376
+ - `getActions(): A` (abstract)
377
+
378
+ Protected helpers:
379
+
380
+ - `getStateValue(): S` (abstract)
381
+ - `setStateValue(next: S): void` (abstract)
382
+ - `initDevTools(options?: { enabled?: boolean; namespace: string }): void`
383
+ - `bindSubscribable<T>(service: { subscribe: (listener: (value: T) => void) => () => void; getSnapshot?: () => T }, onChange: (value: T) => void): void`
384
+ - Registers the subscription on `this.subscriptions` and invokes `onChange` with the current snapshot when available.
385
+
386
+ ### `ObservableStateHandler<S, A>`
387
+
388
+ RxJS-backed handler. Extends `BaseStateHandler`.
389
+
390
+ Constructor:
391
+
392
+ ```ts
393
+ protected constructor({
394
+ initialState,
395
+ options
396
+ }: {
397
+ initialState: S;
398
+ options?: {
399
+ devTools?: { enabled?: boolean; namespace: string };
400
+ };
401
+ })
402
+ ```
403
+
404
+ Public methods:
405
+
406
+ - `getStateAsObservable(options?: { useDistinctUntilChanged?: boolean }): Observable<S>`
407
+ - `getStateItemAsObservable(key: keyof S): Observable<S[keyof S]>`
408
+ - `getObservable(key: keyof S): Observable<S[keyof S]>`
409
+ - `subscribe(listener: () => void): () => void`
410
+
411
+ Notes:
412
+ - The observable stream uses `distinctUntilChanged` by default (JSON compare).
413
+ - `subscribe` does not fire for the initial value; it only fires on subsequent changes.
414
+
415
+ ### `SignalStateHandler<S, A>`
416
+
417
+ Signals-backed handler. Extends `BaseStateHandler`.
418
+
419
+ Constructor:
420
+
421
+ ```ts
422
+ protected constructor({
423
+ initialState,
424
+ options
425
+ }: {
426
+ initialState: S;
427
+ options?: {
428
+ devTools?: { enabled?: boolean; namespace: string };
429
+ useDistinctUntilChanged?: boolean;
430
+ };
431
+ })
432
+ ```
433
+
434
+ Public methods:
435
+
436
+ - `getSignal(): Signal<S>`
437
+ - `subscribe(listener: () => void): () => void`
438
+
439
+ Notes:
440
+ - `useDistinctUntilChanged` defaults to `true` (JSON compare).
441
+
442
+ ### `makeStateSingleton`
443
+
444
+ ```ts
445
+ type StateSingletonOptions = {
446
+ destroyOnNoConsumers?: boolean; // default: true
447
+ };
186
448
 
187
- export function CounterStateFactory(...args) {
188
- return new CounterStateHandler(...args);
449
+ function makeStateSingleton<S, A>(
450
+ factory: () => StateSubscriptionHandler<S, A>,
451
+ options?: StateSingletonOptions
452
+ ): {
453
+ getInstance: () => StateSubscriptionHandler<S, A>;
189
454
  }
190
455
  ```
191
456
 
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).
457
+ Lifecycle behavior:
458
+ - `destroyOnNoConsumers: true` (default): destroy and recreate singleton instances with mount lifecycle.
459
+ - `destroyOnNoConsumers: false`: keep the same singleton instance alive when no component is subscribed.
460
+
461
+ ### Hooks
462
+
463
+ - `useStateHandler<V, A, P extends unknown[]>(factory: (...args: P) => StateSubscriptionHandler<V, A>, params?: P)`
464
+ - Returns `StateSubscriptionHandler<V, A>`.
465
+ - `useStateActions<V, A>(handler: StateSubscriptionHandler<V, A>)`
466
+ - Returns `A`.
467
+ - `useStateSubscription<V, A, Sel = V>(source: StateSubscriptionHandler<V, A> | StateSingleton<V, A>, selector?: (state: V) => Sel, isEqual?: (current: Sel, next: Sel) => boolean)`
468
+ - Returns `[state, actions]`.
469
+ - `useStateFactory<V, A, P extends unknown[], Sel = V>(factory: (...args: P) => StateSubscriptionHandler<V, A>, selector?: (state: V) => Sel, isEqual?: (current: Sel, next: Sel) => boolean, params?: P)`
470
+ - Returns `[state, actions]`.
471
+ - `useStateSingleton<V, A, Sel = V>(singleton: StateSingleton<V, A>, selector?: (state: V) => Sel, isEqual?: (current: Sel, next: Sel) => boolean)`
472
+ - Returns `[state, actions]`.
473
+
474
+ ## Migration
475
+
476
+ From pre-1.0 releases:
477
+
478
+ 1. Rename `StateHandler` -> `ObservableStateHandler`.
479
+ 2. Implement `subscribe()` and `getSnapshot()` on custom handlers.
480
+ 3. Replace `getObservable()` usage with `subscribe()` in custom integrations.
481
+ 4. Update devtools config:
482
+ - From: `super({ initialState, devTools: { ... } })`
483
+ - To: `super({ initialState, options: { devTools: { ... } } })`
Binary file
@@ -0,0 +1,4 @@
1
+ declare global {
2
+ var IS_REACT_ACT_ENVIRONMENT: boolean;
3
+ }
4
+ export {};