@veams/status-quo 1.5.0 → 1.6.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 (126) hide show
  1. package/.turbo/turbo-build.log +12 -0
  2. package/.turbo/turbo-check$colon$types.log +4 -0
  3. package/.turbo/turbo-docs$colon$build.log +14 -0
  4. package/.turbo/turbo-lint.log +8 -0
  5. package/.turbo/turbo-test.log +15 -0
  6. package/CHANGELOG.md +24 -3
  7. package/README.md +217 -41
  8. package/dist/config/status-quo-config.d.ts +13 -0
  9. package/dist/config/status-quo-config.js +14 -0
  10. package/dist/config/status-quo-config.js.map +1 -1
  11. package/dist/hooks/__tests__/state-provider.spec.d.ts +4 -0
  12. package/dist/hooks/__tests__/state-provider.spec.js +179 -0
  13. package/dist/hooks/__tests__/state-provider.spec.js.map +1 -0
  14. package/dist/hooks/__tests__/state-selector.spec.js +11 -12
  15. package/dist/hooks/__tests__/state-selector.spec.js.map +1 -1
  16. package/dist/hooks/__tests__/state-singleton.spec.js +10 -11
  17. package/dist/hooks/__tests__/state-singleton.spec.js.map +1 -1
  18. package/dist/hooks/index.d.ts +1 -0
  19. package/dist/hooks/index.js +1 -0
  20. package/dist/hooks/index.js.map +1 -1
  21. package/dist/hooks/state-factory.js.map +1 -1
  22. package/dist/hooks/state-provider.d.ts +14 -0
  23. package/dist/hooks/state-provider.js +24 -0
  24. package/dist/hooks/state-provider.js.map +1 -0
  25. package/dist/hooks/state-subscription-selector.js +6 -2
  26. package/dist/hooks/state-subscription-selector.js.map +1 -1
  27. package/dist/hooks/state-subscription.js +1 -1
  28. package/dist/hooks/state-subscription.js.map +1 -1
  29. package/dist/index.d.ts +4 -5
  30. package/dist/index.js +2 -3
  31. package/dist/index.js.map +1 -1
  32. package/dist/react/hooks/__tests__/state-provider.spec.d.ts +4 -0
  33. package/dist/react/hooks/__tests__/state-provider.spec.js +179 -0
  34. package/dist/react/hooks/__tests__/state-provider.spec.js.map +1 -0
  35. package/dist/react/hooks/__tests__/state-selector.spec.d.ts +4 -0
  36. package/dist/react/hooks/__tests__/state-selector.spec.js +499 -0
  37. package/dist/react/hooks/__tests__/state-selector.spec.js.map +1 -0
  38. package/dist/react/hooks/__tests__/state-singleton.spec.d.ts +4 -0
  39. package/dist/react/hooks/__tests__/state-singleton.spec.js +96 -0
  40. package/dist/react/hooks/__tests__/state-singleton.spec.js.map +1 -0
  41. package/{src/hooks/index.ts → dist/react/hooks/index.d.ts} +1 -0
  42. package/dist/react/hooks/index.js +7 -0
  43. package/dist/react/hooks/index.js.map +1 -0
  44. package/dist/react/hooks/state-actions.d.ts +2 -0
  45. package/dist/react/hooks/state-actions.js +5 -0
  46. package/dist/react/hooks/state-actions.js.map +1 -0
  47. package/dist/react/hooks/state-factory.d.ts +7 -0
  48. package/dist/react/hooks/state-factory.js +13 -0
  49. package/dist/react/hooks/state-factory.js.map +1 -0
  50. package/dist/react/hooks/state-handler.d.ts +2 -0
  51. package/dist/react/hooks/state-handler.js +9 -0
  52. package/dist/react/hooks/state-handler.js.map +1 -0
  53. package/dist/react/hooks/state-provider.d.ts +14 -0
  54. package/dist/react/hooks/state-provider.js +24 -0
  55. package/dist/react/hooks/state-provider.js.map +1 -0
  56. package/dist/react/hooks/state-singleton.d.ts +6 -0
  57. package/dist/react/hooks/state-singleton.js +7 -0
  58. package/dist/react/hooks/state-singleton.js.map +1 -0
  59. package/dist/react/hooks/state-subscription-selector.d.ts +3 -0
  60. package/dist/react/hooks/state-subscription-selector.js +63 -0
  61. package/dist/react/hooks/state-subscription-selector.js.map +1 -0
  62. package/dist/react/hooks/state-subscription.d.ts +9 -0
  63. package/dist/react/hooks/state-subscription.js +53 -0
  64. package/dist/react/hooks/state-subscription.js.map +1 -0
  65. package/dist/react/index.d.ts +1 -0
  66. package/dist/react/index.js +2 -0
  67. package/dist/react/index.js.map +1 -0
  68. package/dist/store/__tests__/observable-state-handler.spec.js +66 -11
  69. package/dist/store/__tests__/observable-state-handler.spec.js.map +1 -1
  70. package/dist/store/__tests__/signal-state-handler.spec.js.map +1 -1
  71. package/dist/store/base-state-handler.d.ts +4 -6
  72. package/dist/store/base-state-handler.js +10 -9
  73. package/dist/store/base-state-handler.js.map +1 -1
  74. package/dist/store/dev-tools.js +0 -3
  75. package/dist/store/dev-tools.js.map +1 -1
  76. package/dist/store/observable-state-handler.d.ts +4 -10
  77. package/dist/store/observable-state-handler.js +4 -11
  78. package/dist/store/observable-state-handler.js.map +1 -1
  79. package/dist/store/signal-state-handler.d.ts +2 -5
  80. package/dist/store/signal-state-handler.js +3 -2
  81. package/dist/store/signal-state-handler.js.map +1 -1
  82. package/dist/store/state-singleton.js +1 -1
  83. package/dist/store/state-singleton.js.map +1 -1
  84. package/eslint.config.mjs +75 -0
  85. package/package.json +18 -18
  86. package/src/config/status-quo-config.ts +31 -1
  87. package/src/index.ts +11 -15
  88. package/src/react/hooks/__tests__/state-provider.spec.tsx +286 -0
  89. package/src/{hooks → react/hooks}/__tests__/state-selector.spec.tsx +52 -44
  90. package/src/{hooks → react/hooks}/__tests__/state-singleton.spec.tsx +21 -20
  91. package/src/react/hooks/index.ts +11 -0
  92. package/src/{hooks → react/hooks}/state-actions.tsx +1 -1
  93. package/src/{hooks → react/hooks}/state-factory.tsx +2 -2
  94. package/src/{hooks → react/hooks}/state-handler.tsx +1 -1
  95. package/src/react/hooks/state-provider.tsx +56 -0
  96. package/src/{hooks → react/hooks}/state-singleton.tsx +1 -1
  97. package/src/{hooks → react/hooks}/state-subscription-selector.tsx +15 -6
  98. package/src/{hooks → react/hooks}/state-subscription.tsx +5 -9
  99. package/src/react/index.ts +1 -0
  100. package/src/store/__tests__/observable-state-handler.spec.ts +92 -13
  101. package/src/store/__tests__/signal-state-handler.spec.ts +5 -1
  102. package/src/store/base-state-handler.ts +17 -24
  103. package/src/store/dev-tools.ts +3 -3
  104. package/src/store/observable-state-handler.ts +12 -22
  105. package/src/store/signal-state-handler.ts +11 -8
  106. package/src/store/state-singleton.ts +1 -1
  107. package/tsconfig.json +2 -3
  108. package/.eslintrc.cjs +0 -132
  109. package/.github/workflows/pages.yml +0 -46
  110. package/.github/workflows/release.yml +0 -33
  111. package/.nvmrc +0 -1
  112. package/.prettierrc +0 -7
  113. package/docs/assets/index-BBmpszOW.css +0 -1
  114. package/docs/assets/index-Cf8El_RO.js +0 -194
  115. package/docs/assets/statusquo-logo-8GVRbxpc.png +0 -0
  116. package/docs/index.html +0 -13
  117. package/playground/index.html +0 -12
  118. package/playground/src/App.tsx +0 -699
  119. package/playground/src/assets/philosophy-agnostic.svg +0 -18
  120. package/playground/src/assets/philosophy-separation.svg +0 -13
  121. package/playground/src/assets/philosophy-swap.svg +0 -17
  122. package/playground/src/assets/statusquo-logo.png +0 -0
  123. package/playground/src/main.tsx +0 -19
  124. package/playground/src/styles.css +0 -534
  125. package/playground/tsconfig.json +0 -12
  126. package/playground/vite.config.ts +0 -18
@@ -1,699 +0,0 @@
1
- import { useEffect, useState } from 'react';
2
-
3
- import {
4
- makeStateSingleton,
5
- ObservableStateHandler,
6
- SignalStateHandler,
7
- useStateFactory,
8
- useStateSingleton,
9
- } from '@veams/status-quo';
10
-
11
- import Prism from 'prismjs';
12
- import 'prismjs/components/prism-typescript';
13
- import 'prismjs/components/prism-jsx';
14
- import 'prismjs/components/prism-bash';
15
-
16
- import philosophySwap from './assets/philosophy-swap.svg';
17
- import philosophySeparation from './assets/philosophy-separation.svg';
18
- import philosophyAgnostic from './assets/philosophy-agnostic.svg';
19
- import statusQuoLogo from './assets/statusquo-logo.png';
20
-
21
- type CounterState = {
22
- count: number;
23
- };
24
-
25
- type CounterActions = {
26
- increase: () => void;
27
- decrease: () => void;
28
- reset: () => void;
29
- };
30
-
31
- class ObservableCounterStateHandler extends ObservableStateHandler<CounterState, CounterActions> {
32
- constructor(startCount = 0) {
33
- super({
34
- initialState: { count: startCount },
35
- options: { devTools: { enabled: true, namespace: 'Counter as Observable' } },
36
- });
37
- }
38
-
39
- getActions(): CounterActions {
40
- return {
41
- increase: () => {
42
- this.setState({ count: this.getState().count + 1 }, 'increase');
43
- },
44
- decrease: () => {
45
- const current = this.getState().count;
46
- this.setState({ count: Math.max(0, current - 1) }, 'decrease');
47
- },
48
- reset: () => {
49
- this.setState(this.getInitialState(), 'reset');
50
- },
51
- };
52
- }
53
- }
54
-
55
- class SignalCounterStateHandler extends SignalStateHandler<CounterState, CounterActions> {
56
- constructor(startCount = 0) {
57
- super({ initialState: { count: startCount },
58
- options: { devTools: { enabled: true, namespace: 'Counter as Signal' } },
59
- });
60
- }
61
-
62
- getActions(): CounterActions {
63
- return {
64
- increase: () => {
65
- this.setState({ count: this.getState().count + 1 }, 'increase');
66
- },
67
- decrease: () => {
68
- const current = this.getState().count;
69
- this.setState({ count: Math.max(0, current - 1) }, 'decrease');
70
- },
71
- reset: () => {
72
- this.setState(this.getInitialState(), 'reset');
73
- },
74
- };
75
- }
76
- }
77
-
78
- const SingletonCounterStore = makeStateSingleton(() => new ObservableCounterStateHandler(5));
79
-
80
- function ObservableCounterFactory(start = 0) {
81
- return new ObservableCounterStateHandler(start);
82
- }
83
-
84
- function SignalCounterFactory(start = 0) {
85
- return new SignalCounterStateHandler(start);
86
- }
87
-
88
- type CounterCardProps = {
89
- title: string;
90
- subtitle: string;
91
- state: CounterState;
92
- actions: CounterActions;
93
- snippet: string;
94
- };
95
-
96
- function CounterCard({ title, subtitle, state, actions, snippet }: CounterCardProps) {
97
- return (
98
- <section className="card">
99
- <div className="card-header">
100
- <div>
101
- <p className="eyebrow">{subtitle}</p>
102
- <h2>{title}</h2>
103
- </div>
104
- <div className="count-chip">{state.count}</div>
105
- </div>
106
- <pre className="code-block">
107
- <code className="language-ts">{snippet}</code>
108
- </pre>
109
- <div className="count-display">
110
- <span className="count-label">Count</span>
111
- <span className="count-value">{state.count}</span>
112
- </div>
113
- <div className="actions">
114
- <button type="button" className="btn" onClick={actions.decrease}>
115
- Decrease
116
- </button>
117
- <button type="button" className="btn primary" onClick={actions.increase}>
118
- Increase
119
- </button>
120
- <button type="button" className="btn ghost" onClick={actions.reset}>
121
- Reset
122
- </button>
123
- </div>
124
- </section>
125
- );
126
- }
127
-
128
- function SingletonControls() {
129
- const [state, actions] = useStateSingleton(SingletonCounterStore);
130
-
131
- return (
132
- <div className="singleton-card">
133
- <p className="eyebrow">Controller</p>
134
- <h3>Shared Counter</h3>
135
- <p className="muted">Interact with the singleton store.</p>
136
- <div className="singleton-count">{state.count}</div>
137
- <div className="actions">
138
- <button type="button" className="btn" onClick={actions.decrease}>
139
- Decrease
140
- </button>
141
- <button type="button" className="btn primary" onClick={actions.increase}>
142
- Increase
143
- </button>
144
- <button type="button" className="btn ghost" onClick={actions.reset}>
145
- Reset
146
- </button>
147
- </div>
148
- </div>
149
- );
150
- }
151
-
152
- function SingletonDisplay() {
153
- const [state] = useStateSingleton(SingletonCounterStore);
154
-
155
- return (
156
- <div className="singleton-card">
157
- <p className="eyebrow">Viewer</p>
158
- <h3>Another Consumer</h3>
159
- <p className="muted">Updates whenever the singleton changes.</p>
160
- <div className="singleton-count highlight">{state.count}</div>
161
- <div className="singleton-hint">Same store instance, different component.</div>
162
- </div>
163
- );
164
- }
165
-
166
- export function App() {
167
- const [isNavOpen, setIsNavOpen] = useState(false);
168
- const [observableState, observableActions] = useStateFactory(ObservableCounterFactory, [0]);
169
- const [signalState, signalActions] = useStateFactory(SignalCounterFactory, [0]);
170
-
171
- const installSnippet = `npm install @veams/status-quo rxjs @preact/signals-core`;
172
-
173
- const quickstartSnippet = `import { ObservableStateHandler, useStateFactory } from '@veams/status-quo';
174
-
175
- class CounterStore extends ObservableStateHandler<CounterState, CounterActions> {
176
- constructor() {
177
- super({ initialState: { count: 0 } });
178
- }
179
-
180
- getActions() {
181
- return {
182
- increase: () => this.setState({ count: this.getState().count + 1 })
183
- };
184
- }
185
- }
186
-
187
- const [state, actions] = useStateFactory(() => new CounterStore(), []);`;
188
-
189
- const observableSnippet = `class CounterStore extends ObservableStateHandler<CounterState, CounterActions> {
190
- constructor() {
191
- super({ initialState: { count: 0 } });
192
- }
193
-
194
- getActions() {
195
- return {
196
- increase: () => this.setState({ count: this.getState().count + 1 }),
197
- decrease: () => this.setState({ count: this.getState().count - 1 })
198
- };
199
- }
200
- }`;
201
-
202
- const signalSnippet = `class CounterStore extends SignalStateHandler<CounterState, CounterActions> {
203
- constructor() {
204
- super({ initialState: { count: 0 } });
205
- }
206
-
207
- getActions() {
208
- return {
209
- increase: () => this.setState({ count: this.getState().count + 1 }),
210
- decrease: () => this.setState({ count: this.getState().count - 1 })
211
- };
212
- }
213
- }`;
214
-
215
- const devToolsSnippet = `class CounterStore extends ObservableStateHandler<CounterState, CounterActions> {
216
- constructor() {
217
- super({
218
- initialState: { count: 0 },
219
- options: { devTools: { enabled: true, namespace: 'Counter' } }
220
- });
221
- }
222
- }`;
223
-
224
- const cleanupSnippet = `const unsubscribe = store.subscribe(() => {
225
- console.log(store.getSnapshot());
226
- });
227
-
228
- unsubscribe();
229
- store.destroy();`;
230
-
231
- const singletonOptionsSnippet = `import { makeStateSingleton, useStateSingleton } from '@veams/status-quo';
232
-
233
- const SessionSingleton = makeStateSingleton(() => new SessionStore(), {
234
- // true (default): destroy when no consumers are mounted
235
- // false: keep singleton instance alive across unmounts
236
- destroyOnNoConsumers: false
237
- });
238
-
239
- const [state, actions] = useStateSingleton(SessionSingleton);`;
240
-
241
- const apiUseStateHandlerSnippet = `const handler = useStateHandler(createUserStore, []);`;
242
-
243
- const apiUseStateActionsSnippet = `const handler = useStateHandler(createUserStore, []);
244
- const actions = useStateActions(handler);
245
-
246
- actions.rename('Ada');`;
247
-
248
- const apiUseStateSubscriptionSnippet = `const handler = useStateHandler(createUserStore, []);
249
-
250
- const [fullState] = useStateSubscription(handler);
251
- const [name] = useStateSubscription(handler, (state) => state.user.name);
252
- const [profile] = useStateSubscription(
253
- handler,
254
- (state) => state.user.profile,
255
- (current, next) => current.id === next.id && current.role === next.role
256
- );`;
257
-
258
- const apiUseStateFactorySnippet = `const [state, actions] = useStateFactory(createUserStore, []);
259
- const [name] = useStateFactory(createUserStore, (state) => state.user.name, []);
260
- const [profile] = useStateFactory(
261
- createUserStore,
262
- (state) => state.user.profile,
263
- (current, next) => current.id === next.id,
264
- []
265
- );`;
266
-
267
- const apiMakeStateSingletonSnippet = `const SessionSingleton = makeStateSingleton(() => new SessionStore(), {
268
- destroyOnNoConsumers: true // default
269
- });
270
-
271
- const PersistentSessionSingleton = makeStateSingleton(() => new SessionStore(), {
272
- destroyOnNoConsumers: false
273
- });`;
274
-
275
- const apiUseStateSingletonSnippet = `const [state, actions] = useStateSingleton(SessionSingleton);
276
- const [userName] = useStateSingleton(SessionSingleton, (state) => state.user.name);
277
-
278
- // Same behavior via useStateSubscription:
279
- const [session] = useStateSubscription(SessionSingleton);`;
280
-
281
- const apiSetupStatusQuoSnippet = `import equal from 'fast-deep-equal';
282
- import { setupStatusQuo } from '@veams/status-quo';
283
-
284
- setupStatusQuo({
285
- distinct: {
286
- comparator: equal
287
- }
288
- });`;
289
-
290
- const composeSnippet = `import { combineLatest } from "rxjs";
291
-
292
- // RxJS: combine handler streams (RxJS shines here)
293
- class AppSignalStore extends SignalStateHandler<AppState, AppActions> {
294
- private counter$ = CounterObservableStore.getInstance().getStateAsObservable();
295
- private card$ = new CardObservableHandler();
296
-
297
- constructor() {
298
- super({ initialState: { counter: 0, cardTitle: "" }});
299
-
300
- this.subscriptions.push(
301
- combineLatest([
302
- this.counter$,
303
- this.card$,
304
- ]).subscribe(([counterState, cardState]) => {
305
- this.setState({
306
- counter: counterState,
307
- cardTitle: cardState.title,
308
- }, "sync-combined");
309
- })
310
- )
311
- }
312
-
313
- }
314
-
315
- // Signals: combine derived values via computed + bindSubscribable
316
- import { computed } from "@preact/signals-core";
317
-
318
- class AppSignalStore extends SignalStateHandler<AppState, AppActions> {
319
- private counter = CounterSignalHandler.getInstance().getSignal();
320
- private card = new CardSignalHandler();
321
- private combined = computed(() => ({
322
- counter: this.counter.getSignal().value,
323
- cardTitle: this.card.getSignal().value.title,
324
- }));
325
-
326
- constructor() {
327
- super({ initialState: { counter: 0, cardTitle: "" }});
328
-
329
- this.bindSubscribable(
330
- { subscribe: this.combined.subscribe.bind(this.combined), getSnapshot: () => this.combined.value },
331
- (nextState) => this.setState(nextState, "sync-combined")
332
- );
333
- }
334
- }`;
335
-
336
- useEffect(() => {
337
- Prism.highlightAll();
338
- }, []);
339
-
340
- const closeNav = () => {
341
- setIsNavOpen(false);
342
- };
343
-
344
- return (
345
- <div className="app">
346
- <div className="brand-bar">
347
- <img src={statusQuoLogo} alt="StatusQuo logo" className="brand-logo" />
348
- </div>
349
- <nav className="nav">
350
- <button
351
- type="button"
352
- className="nav-toggle"
353
- aria-expanded={isNavOpen}
354
- aria-controls="docs-nav-links"
355
- onClick={() => setIsNavOpen((open) => !open)}
356
- >
357
- Sections
358
- </button>
359
- <div id="docs-nav-links" className={`nav-links${isNavOpen ? ' is-open' : ''}`}>
360
- <a href="#overview" onClick={closeNav}>
361
- Overview
362
- </a>
363
- <a href="#quickstart" onClick={closeNav}>
364
- Quickstart
365
- </a>
366
- <a href="#factory" onClick={closeNav}>
367
- Factory
368
- </a>
369
- <a href="#selectors" onClick={closeNav}>
370
- Selectors
371
- </a>
372
- <a href="#api" onClick={closeNav}>
373
- API
374
- </a>
375
- <a href="#demo" onClick={closeNav}>
376
- Demo
377
- </a>
378
- <a href="#singleton" onClick={closeNav}>
379
- Singleton
380
- </a>
381
- <a href="#singleton-demo" onClick={closeNav}>
382
- Singleton demo
383
- </a>
384
- <a href="#compose" onClick={closeNav}>
385
- Compose
386
- </a>
387
- <a href="#devtools" onClick={closeNav}>
388
- Devtools
389
- </a>
390
- <a href="#cleanup" onClick={closeNav}>
391
- Cleanup
392
- </a>
393
- </div>
394
- </nav>
395
- <header id="overview" className="hero intro">
396
- <div>
397
- <p className="eyebrow">Philosophy</p>
398
- <h1>State management that stays out of your way</h1>
399
- <p className="subtext">
400
- <span>StatusQuo</span> treats state handlers as small, composable objects with explicit
401
- lifecycle and a tiny interface. Components subscribe to snapshots, not
402
- framework‑specific store APIs. That makes it easy to swap the engine under the hood—RxJS
403
- for observable streams or Preact Signals for ultra‑light reactive state.
404
- </p>
405
- <p className="subtext">
406
- Handlers encapsulate state transitions, expose actions, and clean up after themselves.
407
- You decide whether each component should have its own instance (factory) or share a
408
- singleton, while the UI stays blissfully unaware of the chosen implementation.
409
- </p>
410
- </div>
411
- </header>
412
-
413
- <section className="philosophy-grid">
414
- <article className="philosophy-card">
415
- <img src={philosophySwap} alt="Swapping state engines illustration" />
416
- <div>
417
- <h3>Swap the engine, keep the API</h3>
418
- <p>
419
- Move between RxJS observables and Signals without rewriting your components. Stores
420
- keep the same shape; only the internal runtime changes.
421
- </p>
422
- </div>
423
- </article>
424
- <article className="philosophy-card">
425
- <img src={philosophySeparation} alt="View and state separation illustration" />
426
- <div>
427
- <h3>Separate view and state</h3>
428
- <p>
429
- Handlers own transitions and expose actions. Views subscribe to snapshots and never
430
- touch store internals.
431
- </p>
432
- </div>
433
- </article>
434
- <article className="philosophy-card">
435
- <img src={philosophyAgnostic} alt="Framework-agnostic state core illustration" />
436
- <div>
437
- <h3>Framework‑agnostic core</h3>
438
- <p>
439
- Your state logic lives outside any UI library. Hooks provide the glue, not the
440
- business logic.
441
- </p>
442
- </div>
443
- </article>
444
- </section>
445
-
446
- <section id="quickstart" className="doc-section">
447
- <div className="doc-copy">
448
- <p className="eyebrow">Quickstart</p>
449
- <h2>Install and wire up a handler</h2>
450
- <p>
451
- Pick either observable or signal stores. The hooks stay the same, so you can swap the
452
- backend without changing your components.
453
- </p>
454
- </div>
455
- <div className="doc-snippets">
456
- <pre className="code-block compact">
457
- <code className="language-bash">{installSnippet}</code>
458
- </pre>
459
- <pre className="code-block compact">
460
- <code className="language-ts">{quickstartSnippet}</code>
461
- </pre>
462
- </div>
463
- </section>
464
-
465
- <section className="guide">
466
- <article id="factory" className="guide-card">
467
- <div>
468
- <p className="eyebrow">Factory</p>
469
- <h2>Fresh handler per mount</h2>
470
- <p>
471
- Use `useStateFactory` when each component instance needs its own store.
472
- </p>
473
- <p className="guide-highlight">Local state, zero cross-talk, predictable lifecycles.</p>
474
- </div>
475
- </article>
476
- <article id="singleton" className="guide-card">
477
- <div>
478
- <p className="eyebrow">Singleton</p>
479
- <h2>One handler, many consumers</h2>
480
- <p>
481
- Use `makeStateSingleton` + `useStateSingleton` for shared state across components.
482
- </p>
483
- <p className="guide-highlight">One source of truth, synced everywhere.</p>
484
- </div>
485
- </article>
486
- <article id="selectors" className="guide-card">
487
- <div>
488
- <p className="eyebrow">Selectors</p>
489
- <h2>Avoid rerender fanout</h2>
490
- <p>
491
- Subscribe only to the state slice you need with selector + equality functions.
492
- </p>
493
- <p className="guide-highlight">Read less, rerender less, ship smoother UIs.</p>
494
- </div>
495
- </article>
496
- </section>
497
-
498
- <section id="api" className="doc-section">
499
- <div className="doc-copy">
500
- <p className="eyebrow">API</p>
501
- <h2>Detailed hook and singleton reference</h2>
502
- </div>
503
- <div className="api-flow">
504
- <div className="api-group">
505
- <article className="api-composition-card">
506
- <h3>Base composition</h3>
507
- <p>
508
- Build explicit lifecycles with `useStateHandler`, pull actions with
509
- `useStateActions`, and subscribe to exact slices with `useStateSubscription`.
510
- </p>
511
- <p className="api-composition-punch">Maximum control, minimum magic.</p>
512
- </article>
513
- <div className="api-grid">
514
- <article className="api-card">
515
- <h3>useStateHandler(factory, params?)</h3>
516
- <p>Creates one handler instance per component mount and reuses it across rerenders.</p>
517
- <pre className="code-block compact">
518
- <code className="language-ts">{apiUseStateHandlerSnippet}</code>
519
- </pre>
520
- </article>
521
-
522
- <article className="api-card">
523
- <h3>useStateActions(handler)</h3>
524
- <p>Returns actions without subscribing to state updates.</p>
525
- <pre className="code-block compact">
526
- <code className="language-ts">{apiUseStateActionsSnippet}</code>
527
- </pre>
528
- </article>
529
-
530
- <article className="api-card api-card-wide">
531
- <h3>useStateSubscription(source, selector?, isEqual?)</h3>
532
- <p>
533
- Core subscription hook. Source can be a handler or singleton. Returns
534
- `[selectedState, actions]`.
535
- </p>
536
- <pre className="code-block compact">
537
- <code className="language-ts">{apiUseStateSubscriptionSnippet}</code>
538
- </pre>
539
- </article>
540
- </div>
541
- </div>
542
-
543
- <div className="api-group">
544
- <article className="api-composition-card">
545
- <h3>Shortcut composition</h3>
546
- <p>
547
- Reduce wiring with `useStateFactory` and `useStateSingleton` when you want
548
- the same behavior in a smaller API surface.
549
- </p>
550
- <p className="api-composition-punch">Less wiring, same behavior.</p>
551
- </article>
552
- <div className="api-grid">
553
- <article className="api-card">
554
- <h3>useStateFactory(factory, selector?, isEqual?, params?)</h3>
555
- <p>
556
- Shortcut for handler creation + subscription. Supports full state and selected
557
- slices.
558
- </p>
559
- <pre className="code-block compact">
560
- <code className="language-ts">{apiUseStateFactorySnippet}</code>
561
- </pre>
562
- </article>
563
-
564
- <article className="api-card">
565
- <h3>useStateSingleton(singleton, selector?, isEqual?)</h3>
566
- <p>
567
- Shortcut for subscribing to a singleton. Selector and equality semantics match
568
- `useStateSubscription`.
569
- </p>
570
- <pre className="code-block compact">
571
- <code className="language-ts">{apiUseStateSingletonSnippet}</code>
572
- </pre>
573
- </article>
574
- </div>
575
- </div>
576
-
577
- <div className="api-group">
578
- <article className="api-composition-card">
579
- <h3>Helper function</h3>
580
- <p>
581
- Configure global distinct behavior once with `setupStatusQuo` and create shared
582
- singleton providers with `makeStateSingleton`.
583
- </p>
584
- <p className="api-composition-punch">One setup, consistent behavior.</p>
585
- </article>
586
- <div className="api-grid">
587
- <article className="api-card">
588
- <h3>setupStatusQuo(config?)</h3>
589
- <p>
590
- Defines global runtime defaults for distinct update behavior. Per-handler options
591
- still override global config.
592
- </p>
593
- <pre className="code-block compact">
594
- <code className="language-ts">{apiSetupStatusQuoSnippet}</code>
595
- </pre>
596
- </article>
597
-
598
- <article className="api-card">
599
- <h3>makeStateSingleton(factory, options?)</h3>
600
- <p>
601
- Creates a shared handler provider. `destroyOnNoConsumers` controls instance
602
- teardown.
603
- </p>
604
- <pre className="code-block compact">
605
- <code className="language-ts">{apiMakeStateSingletonSnippet}</code>
606
- </pre>
607
- </article>
608
- </div>
609
- </div>
610
- </div>
611
- </section>
612
-
613
- <div id="demo" className="grid">
614
- <CounterCard
615
- title="Observable State Handler"
616
- subtitle="RxJS BehaviorSubject"
617
- state={observableState}
618
- actions={observableActions}
619
- snippet={observableSnippet}
620
- />
621
- <CounterCard
622
- title="Signal State Handler"
623
- subtitle="Preact Signals"
624
- state={signalState}
625
- actions={signalActions}
626
- snippet={signalSnippet}
627
- />
628
- </div>
629
-
630
- <section id="singleton-demo" className="doc-section">
631
- <div className="doc-copy">
632
- <p className="eyebrow">Singleton demo</p>
633
- <h2>One store, multiple components</h2>
634
- <p>
635
- Both panels below are subscribed to the same singleton handler. Update the controller
636
- and watch the viewer react instantly. The handler is only disposed once the last
637
- consumer unmounts (unless persistence is enabled).
638
- </p>
639
- <p>
640
- `makeStateSingleton(factory, options)` always returns one shared instance.
641
- `destroyOnNoConsumers` controls whether that instance is torn down when the last
642
- `useStateSingleton` consumer unmounts.
643
- </p>
644
- </div>
645
- <pre className="code-block compact">
646
- <code className="language-ts">{singletonOptionsSnippet}</code>
647
- </pre>
648
- <div className="singleton-grid">
649
- <SingletonControls />
650
- <SingletonDisplay />
651
- </div>
652
- </section>
653
-
654
- <section id="compose" className="doc-section">
655
- <div className="doc-copy">
656
- <p className="eyebrow">Compose</p>
657
- <h2>Combine multiple handlers</h2>
658
- <p>
659
- Use only the slice you need. RxJS makes multi-source composition powerful and
660
- declarative with operators like `combineLatest`, `switchMap`, or `debounceTime`. Signals
661
- can derive values with `computed` and wire them into a parent store via
662
- `bindSubscribable`. This keeps parent stores lean and focused.
663
- </p>
664
- </div>
665
- <pre className="code-block">
666
- <code className="language-ts">{composeSnippet}</code>
667
- </pre>
668
- </section>
669
-
670
- <section id="devtools" className="doc-section">
671
- <div className="doc-copy">
672
- <p className="eyebrow">Devtools</p>
673
- <h2>Redux devtools integration</h2>
674
- <p>
675
- Enable devtools to inspect actions and state transitions. Both handlers share the same
676
- options shape.
677
- </p>
678
- </div>
679
- <pre className="code-block">
680
- <code className="language-ts">{devToolsSnippet}</code>
681
- </pre>
682
- </section>
683
-
684
- <section id="cleanup" className="doc-section">
685
- <div className="doc-copy">
686
- <p className="eyebrow">Cleanup</p>
687
- <h2>Controlled teardown</h2>
688
- <p>
689
- Each handler exposes `subscribe`, `getSnapshot`, and `destroy`. Use them when you wire
690
- custom integrations or combine multiple stores manually.
691
- </p>
692
- </div>
693
- <pre className="code-block">
694
- <code className="language-ts">{cleanupSnippet}</code>
695
- </pre>
696
- </section>
697
- </div>
698
- );
699
- }