@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.
- package/CHANGELOG.md +45 -0
- package/README.md +420 -130
- package/assets/statusquo-logo.png +0 -0
- package/dist/hooks/__tests__/state-selector.spec.d.ts +4 -0
- package/dist/hooks/__tests__/state-selector.spec.js +384 -0
- package/dist/hooks/__tests__/state-selector.spec.js.map +1 -0
- package/dist/hooks/__tests__/state-singleton.spec.d.ts +4 -0
- package/dist/hooks/__tests__/state-singleton.spec.js +97 -0
- package/dist/hooks/__tests__/state-singleton.spec.js.map +1 -0
- package/dist/hooks/index.d.ts +3 -0
- package/dist/hooks/index.js +3 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/state-actions.d.ts +2 -0
- package/dist/hooks/state-actions.js +5 -0
- package/dist/hooks/state-actions.js.map +1 -0
- package/dist/hooks/state-factory.d.ts +5 -0
- package/dist/hooks/state-factory.js +10 -6
- package/dist/hooks/state-factory.js.map +1 -1
- package/dist/hooks/state-handler.d.ts +2 -0
- package/dist/hooks/state-handler.js +9 -0
- package/dist/hooks/state-handler.js.map +1 -0
- package/dist/hooks/state-singleton.d.ts +4 -0
- package/dist/hooks/state-singleton.js +3 -5
- package/dist/hooks/state-singleton.js.map +1 -1
- package/dist/hooks/state-subscription-selector.d.ts +5 -0
- package/dist/hooks/state-subscription-selector.js +29 -0
- package/dist/hooks/state-subscription-selector.js.map +1 -0
- package/dist/hooks/state-subscription.d.ts +8 -1
- package/dist/hooks/state-subscription.js +49 -10
- package/dist/hooks/state-subscription.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/store/__tests__/observable-state-handler.spec.js +4 -0
- package/dist/store/__tests__/observable-state-handler.spec.js.map +1 -1
- package/dist/store/base-state-handler.d.ts +4 -0
- package/dist/store/base-state-handler.js +6 -0
- package/dist/store/base-state-handler.js.map +1 -1
- package/dist/store/index.d.ts +1 -1
- package/dist/store/observable-state-handler.d.ts +2 -0
- package/dist/store/observable-state-handler.js +5 -1
- package/dist/store/observable-state-handler.js.map +1 -1
- package/dist/store/state-singleton.d.ts +4 -1
- package/dist/store/state-singleton.js +11 -2
- package/dist/store/state-singleton.js.map +1 -1
- package/docs/assets/index-Ci4A1zSh.js +187 -0
- package/docs/assets/index-WFTLEHd3.css +1 -0
- package/docs/assets/statusquo-logo-8GVRbxpc.png +0 -0
- package/docs/index.html +13 -0
- package/package.json +1 -1
- package/playground/src/App.tsx +284 -81
- package/playground/src/assets/statusquo-logo.png +0 -0
- package/playground/src/styles.css +137 -6
- package/src/hooks/__tests__/state-selector.spec.tsx +607 -0
- package/src/hooks/__tests__/state-singleton.spec.tsx +151 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/state-actions.tsx +7 -0
- package/src/hooks/state-factory.tsx +32 -6
- package/src/hooks/state-handler.tsx +16 -0
- package/src/hooks/state-singleton.tsx +17 -7
- package/src/hooks/state-subscription-selector.tsx +70 -0
- package/src/hooks/state-subscription.tsx +98 -21
- package/src/index.ts +12 -3
- package/src/store/__tests__/observable-state-handler.spec.ts +6 -0
- package/src/store/base-state-handler.ts +9 -0
- package/src/store/index.ts +1 -1
- package/src/store/observable-state-handler.ts +6 -1
- 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
|
-
|
|
1
|
+
<center>
|
|
2
|
+
<img src="assets/statusquo-logo.png" width="200" alt="StatusQuo Logo" style="margin: 0 auto;">
|
|
3
|
+
</center>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
# @veams/status-quo
|
|
6
|
+
[](https://www.npmjs.com/package/@veams/status-quo)
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
The manager to rule your state.
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
This page mirrors the demo content and adds a full API reference.
|
|
8
11
|
|
|
9
|
-
|
|
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
|
-
##
|
|
28
|
+
## Overview
|
|
15
29
|
|
|
16
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
44
|
+
## Quickstart
|
|
37
45
|
|
|
38
|
-
|
|
39
|
-
- `SignalStateHandler` (Signals-backed)
|
|
46
|
+
Install:
|
|
40
47
|
|
|
41
|
-
|
|
48
|
+
```bash
|
|
49
|
+
npm install @veams/status-quo rxjs @preact/signals-core
|
|
50
|
+
```
|
|
42
51
|
|
|
43
|
-
|
|
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 {
|
|
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
|
|
58
|
-
constructor(
|
|
59
|
-
super({ initialState: { count:
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
83
|
-
return new CounterStateHandler(...args);
|
|
84
|
-
}
|
|
77
|
+
const [state, actions] = useStateFactory(() => new CounterStore(), []);
|
|
85
78
|
```
|
|
86
79
|
|
|
87
|
-
|
|
80
|
+
## Handlers
|
|
88
81
|
|
|
89
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
116
|
+
## Singletons
|
|
107
117
|
|
|
108
|
-
|
|
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
|
-
|
|
129
|
+
Keep a singleton instance alive across unmounts:
|
|
114
130
|
|
|
115
|
-
|
|
131
|
+
```ts
|
|
132
|
+
const PersistentCounterSingleton = makeStateSingleton(() => new CounterStore(), {
|
|
133
|
+
destroyOnNoConsumers: false,
|
|
134
|
+
});
|
|
116
135
|
```
|
|
117
136
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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:
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
188
|
-
|
|
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
|
-
|
|
193
|
-
|
|
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
|