@veams/status-quo 1.5.1 → 1.7.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 (127) 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 +547 -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 +114 -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 +3 -5
  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 +118 -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/react/hooks/state-subscription-selector.tsx +190 -0
  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 -22
  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
  127. package/src/hooks/state-subscription-selector.tsx +0 -111
@@ -0,0 +1,286 @@
1
+ import React, { act } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+
4
+ import {
5
+ StateProvider,
6
+ useProvidedStateActions,
7
+ useProvidedStateHandler,
8
+ useProvidedStateSubscription,
9
+ } from '../state-provider.js';
10
+
11
+ import type { StateSubscriptionHandler } from '../../../types/types.js';
12
+
13
+ declare global {
14
+ // React 19 requires this flag in test environments that use manual act() calls.
15
+
16
+ var IS_REACT_ACT_ENVIRONMENT: boolean;
17
+ }
18
+
19
+ type TestState = {
20
+ count: number;
21
+ label: string;
22
+ };
23
+
24
+ type TestActions = {
25
+ increment: () => void;
26
+ rename: (label: string) => void;
27
+ };
28
+
29
+ class TestStateHandler implements StateSubscriptionHandler<TestState, TestActions> {
30
+ private readonly initialState: TestState;
31
+ private state: TestState;
32
+ private readonly listeners = new Set<(value: TestState) => void>();
33
+
34
+ destroy = jest.fn();
35
+
36
+ constructor(initialState: TestState) {
37
+ this.initialState = initialState;
38
+ this.state = initialState;
39
+ }
40
+
41
+ subscribe(listener: () => void): () => void;
42
+ subscribe(listener: (value: TestState) => void): () => void;
43
+ subscribe(listener: ((value: TestState) => void) | (() => void)) {
44
+ const typedListener = listener as (value: TestState) => void;
45
+ this.listeners.add(typedListener);
46
+
47
+ return () => {
48
+ this.listeners.delete(typedListener);
49
+ };
50
+ }
51
+
52
+ getSnapshot = () => {
53
+ return this.state;
54
+ };
55
+
56
+ getInitialState = () => {
57
+ return this.initialState;
58
+ };
59
+
60
+ getActions = () => {
61
+ return {
62
+ increment: () => {
63
+ this.state = {
64
+ ...this.state,
65
+ count: this.state.count + 1,
66
+ };
67
+
68
+ this.emitStateChange();
69
+ },
70
+ rename: (label: string) => {
71
+ this.state = {
72
+ ...this.state,
73
+ label,
74
+ };
75
+
76
+ this.emitStateChange();
77
+ },
78
+ };
79
+ };
80
+
81
+ private emitStateChange() {
82
+ const nextState = this.state;
83
+ this.listeners.forEach((listener) => listener(nextState));
84
+ }
85
+ }
86
+
87
+ function CountConsumer({ onRender }: { onRender: (count: number) => void }) {
88
+ const [count] = useProvidedStateSubscription<TestState, TestActions, number>(
89
+ (state) => state.count
90
+ );
91
+
92
+ onRender(count);
93
+
94
+ return <span>{count}</span>;
95
+ }
96
+
97
+ function FullStateConsumer({
98
+ onActionsReady,
99
+ onRender,
100
+ }: {
101
+ onActionsReady: (actions: TestActions) => void;
102
+ onRender: (state: TestState) => void;
103
+ }) {
104
+ const [state, actions] = useProvidedStateSubscription<TestState, TestActions>();
105
+
106
+ onRender(state);
107
+ onActionsReady(actions);
108
+
109
+ return <span>{state.label}</span>;
110
+ }
111
+
112
+ function ActionsOnlyConsumer({
113
+ onActionsReady,
114
+ onRender,
115
+ }: {
116
+ onActionsReady: (actions: TestActions) => void;
117
+ onRender: () => void;
118
+ }) {
119
+ const actions = useProvidedStateActions<TestState, TestActions>();
120
+
121
+ onRender();
122
+ onActionsReady(actions);
123
+
124
+ return <span>actions-only</span>;
125
+ }
126
+
127
+ function HandlerConsumer({
128
+ onHandlerReady,
129
+ }: {
130
+ onHandlerReady: (handler: StateSubscriptionHandler<TestState, TestActions>) => void;
131
+ }) {
132
+ const handler = useProvidedStateHandler<TestState, TestActions>();
133
+
134
+ onHandlerReady(handler);
135
+
136
+ return <span>handler</span>;
137
+ }
138
+
139
+ function MissingProviderConsumer() {
140
+ useProvidedStateActions<TestState, TestActions>();
141
+
142
+ return null;
143
+ }
144
+
145
+ describe('StateProvider', () => {
146
+ let container: HTMLDivElement;
147
+ let root: ReturnType<typeof createRoot>;
148
+
149
+ beforeAll(() => {
150
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
151
+ });
152
+
153
+ beforeEach(() => {
154
+ container = document.createElement('div');
155
+ document.body.appendChild(container);
156
+ root = createRoot(container);
157
+ });
158
+
159
+ afterEach(() => {
160
+ act(() => {
161
+ root.unmount();
162
+ });
163
+
164
+ container.remove();
165
+ });
166
+
167
+ afterAll(() => {
168
+ globalThis.IS_REACT_ACT_ENVIRONMENT = false;
169
+ });
170
+
171
+ it('should share one handler instance across the provider subtree', () => {
172
+ const stateHandler = new TestStateHandler({
173
+ count: 0,
174
+ label: 'Counter',
175
+ });
176
+ const countRenderSpy = jest.fn<void, [number]>();
177
+ const actionsRenderSpy = jest.fn();
178
+ const actionsReadySpy = jest.fn<void, [TestActions]>();
179
+ const handlerReadySpy = jest.fn<void, [StateSubscriptionHandler<TestState, TestActions>]>();
180
+
181
+ act(() => {
182
+ root.render(
183
+ <StateProvider instance={stateHandler}>
184
+ <CountConsumer onRender={countRenderSpy} />
185
+ <ActionsOnlyConsumer onActionsReady={actionsReadySpy} onRender={actionsRenderSpy} />
186
+ <HandlerConsumer onHandlerReady={handlerReadySpy} />
187
+ </StateProvider>
188
+ );
189
+ });
190
+
191
+ expect(countRenderSpy).toHaveBeenCalledTimes(1);
192
+ expect(countRenderSpy).toHaveBeenLastCalledWith(0);
193
+ expect(actionsRenderSpy).toHaveBeenCalledTimes(1);
194
+ expect(handlerReadySpy).toHaveBeenCalledWith(stateHandler);
195
+
196
+ const [[actions]] = actionsReadySpy.mock.calls as [[TestActions]];
197
+
198
+ act(() => {
199
+ actions.rename('Renamed');
200
+ });
201
+
202
+ expect(countRenderSpy).toHaveBeenCalledTimes(1);
203
+ expect(actionsRenderSpy).toHaveBeenCalledTimes(1);
204
+
205
+ act(() => {
206
+ actions.increment();
207
+ });
208
+
209
+ expect(countRenderSpy).toHaveBeenCalledTimes(2);
210
+ expect(countRenderSpy).toHaveBeenLastCalledWith(1);
211
+ expect(actionsRenderSpy).toHaveBeenCalledTimes(1);
212
+ });
213
+
214
+ it('should return the full snapshot when no selector is provided', () => {
215
+ const stateHandler = new TestStateHandler({
216
+ count: 2,
217
+ label: 'Counter',
218
+ });
219
+ const renderSpy = jest.fn<void, [TestState]>();
220
+ const actionsReadySpy = jest.fn<void, [TestActions]>();
221
+
222
+ act(() => {
223
+ root.render(
224
+ <StateProvider instance={stateHandler}>
225
+ <FullStateConsumer onActionsReady={actionsReadySpy} onRender={renderSpy} />
226
+ </StateProvider>
227
+ );
228
+ });
229
+
230
+ expect(renderSpy).toHaveBeenCalledTimes(1);
231
+ expect(renderSpy).toHaveBeenLastCalledWith({ count: 2, label: 'Counter' });
232
+
233
+ const [[actions]] = actionsReadySpy.mock.calls as [[TestActions]];
234
+
235
+ act(() => {
236
+ actions.increment();
237
+ });
238
+
239
+ expect(renderSpy).toHaveBeenCalledTimes(2);
240
+ expect(renderSpy).toHaveBeenLastCalledWith({ count: 3, label: 'Counter' });
241
+ });
242
+
243
+ it('should follow a new instance when the provider instance changes', () => {
244
+ const firstHandler = new TestStateHandler({
245
+ count: 1,
246
+ label: 'First',
247
+ });
248
+ const secondHandler = new TestStateHandler({
249
+ count: 8,
250
+ label: 'Second',
251
+ });
252
+ const renderSpy = jest.fn<void, [number]>();
253
+
254
+ act(() => {
255
+ root.render(
256
+ <StateProvider instance={firstHandler}>
257
+ <CountConsumer onRender={renderSpy} />
258
+ </StateProvider>
259
+ );
260
+ });
261
+
262
+ act(() => {
263
+ root.render(
264
+ <StateProvider instance={secondHandler}>
265
+ <CountConsumer onRender={renderSpy} />
266
+ </StateProvider>
267
+ );
268
+ });
269
+
270
+ expect(renderSpy).toHaveBeenCalledTimes(2);
271
+ expect(renderSpy).toHaveBeenNthCalledWith(1, 1);
272
+ expect(renderSpy).toHaveBeenNthCalledWith(2, 8);
273
+ });
274
+
275
+ it('should throw when provider hooks are used outside StateProvider', () => {
276
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined);
277
+
278
+ expect(() => {
279
+ act(() => {
280
+ root.render(<MissingProviderConsumer />);
281
+ });
282
+ }).toThrow('No StateProvider instance found in the current React tree.');
283
+
284
+ consoleErrorSpy.mockRestore();
285
+ });
286
+ });
@@ -1,21 +1,19 @@
1
- import React from 'react';
2
- import { act } from 'react';
1
+ import React, { act } from 'react';
3
2
  import { createRoot } from 'react-dom/client';
4
3
 
5
- import { makeStateSingleton } from '../../store/state-singleton.js';
6
-
4
+ import { makeStateSingleton } from '../../../store/state-singleton.js';
7
5
  import { useStateActions } from '../state-actions.js';
8
6
  import { useStateFactory } from '../state-factory.js';
9
7
  import { useStateHandler } from '../state-handler.js';
10
8
  import { useStateSingleton } from '../state-singleton.js';
11
9
  import { useStateSubscription } from '../state-subscription.js';
12
10
 
13
- import type { StateSingleton } from '../../store/state-singleton.js';
14
- import type { StateSubscriptionHandler } from '../../types/types.js';
11
+ import type { StateSingleton } from '../../../store/state-singleton.js';
12
+ import type { StateSubscriptionHandler } from '../../../types/types.js';
15
13
 
16
14
  declare global {
17
15
  // React 19 requires this flag in test environments that use manual act() calls.
18
- // eslint-disable-next-line no-var
16
+
19
17
  var IS_REACT_ACT_ENVIRONMENT: boolean;
20
18
  }
21
19
 
@@ -153,9 +151,10 @@ class CounterStateHandler implements StateSubscriptionHandler<CounterState, Coun
153
151
  };
154
152
  }
155
153
 
156
- class CounterMirrorStateHandler
157
- implements StateSubscriptionHandler<CounterMirrorState, CounterMirrorActions>
158
- {
154
+ class CounterMirrorStateHandler implements StateSubscriptionHandler<
155
+ CounterMirrorState,
156
+ CounterMirrorActions
157
+ > {
159
158
  private readonly initialState: CounterMirrorState;
160
159
  private state: CounterMirrorState;
161
160
  private readonly listeners = new Set<(value: CounterMirrorState) => void>();
@@ -173,13 +172,15 @@ class CounterMirrorStateHandler
173
172
  mirroredCount: initialCounterState.count,
174
173
  };
175
174
  this.state = this.initialState;
176
- this.unsubscribeFromCounter = counterStateHandler.subscribe((nextCounterState: CounterState) => {
177
- this.state = {
178
- mirroredCount: nextCounterState.count,
179
- };
180
- const nextMirrorState = this.state;
181
- this.listeners.forEach((listener) => listener(nextMirrorState));
182
- });
175
+ this.unsubscribeFromCounter = counterStateHandler.subscribe(
176
+ (nextCounterState: CounterState) => {
177
+ this.state = {
178
+ mirroredCount: nextCounterState.count,
179
+ };
180
+ const nextMirrorState = this.state;
181
+ this.listeners.forEach((listener) => listener(nextMirrorState));
182
+ }
183
+ );
183
184
  }
184
185
 
185
186
  subscribe(listener: () => void): () => void;
@@ -235,7 +236,11 @@ type ActionsOnlyConsumerProps = {
235
236
  onActionsReady: (actions: TestActions) => void;
236
237
  };
237
238
 
238
- const ActionsOnlyConsumer = ({ createStateHandler, onRender, onActionsReady }: ActionsOnlyConsumerProps) => {
239
+ const ActionsOnlyConsumer = ({
240
+ createStateHandler,
241
+ onRender,
242
+ onActionsReady,
243
+ }: ActionsOnlyConsumerProps) => {
239
244
  const stateHandler = useStateHandler(createStateHandler, []);
240
245
  const actions = useStateActions(stateHandler);
241
246
 
@@ -250,13 +255,11 @@ type FactoryShortcutConsumerProps = {
250
255
  onRender: (value: string) => void;
251
256
  };
252
257
 
253
- const FactoryShortcutConsumer = ({ createStateHandler, onRender }: FactoryShortcutConsumerProps) => {
254
- const [userName] = useStateFactory(
255
- createStateHandler,
256
- (state) => state.user.name,
257
- Object.is,
258
- []
259
- );
258
+ const FactoryShortcutConsumer = ({
259
+ createStateHandler,
260
+ onRender,
261
+ }: FactoryShortcutConsumerProps) => {
262
+ const [userName] = useStateFactory(createStateHandler, (state) => state.user.name, Object.is, []);
260
263
  onRender(userName);
261
264
 
262
265
  return <span>{userName}</span>;
@@ -328,6 +331,23 @@ const FullSubscriptionConsumer = ({
328
331
  return <span>{state.user.name}</span>;
329
332
  };
330
333
 
334
+ type ObjectSelectorConsumerProps = {
335
+ createStateHandler: () => StateSubscriptionHandler<TestState, TestActions>;
336
+ onRender: (value: { counter: number; userName: string }) => void;
337
+ };
338
+
339
+ const ObjectSelectorConsumer = ({ createStateHandler, onRender }: ObjectSelectorConsumerProps) => {
340
+ const stateHandler = useStateHandler(createStateHandler, []);
341
+ const [summary] = useStateSubscription(stateHandler, (state) => ({
342
+ counter: state.counter,
343
+ userName: state.user.name,
344
+ }));
345
+
346
+ onRender(summary);
347
+
348
+ return <span>{summary.userName}</span>;
349
+ };
350
+
331
351
  type StrictModeMirrorFactoryConsumerProps = {
332
352
  createStateHandler: () => StateSubscriptionHandler<CounterMirrorState, CounterMirrorActions>;
333
353
  onRender: (count: number) => void;
@@ -409,7 +429,7 @@ describe('Selector hooks', () => {
409
429
  expect(createStateHandler).toHaveBeenCalledTimes(1);
410
430
 
411
431
  act(() => {
412
- root.render(<></>);
432
+ root.render(<React.Fragment />);
413
433
  });
414
434
 
415
435
  await act(async () => {
@@ -434,7 +454,7 @@ describe('Selector hooks', () => {
434
454
  return stateHandler;
435
455
  });
436
456
  const stateRenderSpy = jest.fn();
437
- const actionsReadySpy = jest.fn();
457
+ const actionsReadySpy = jest.fn<void, [TestActions]>();
438
458
 
439
459
  act(() => {
440
460
  root.render(
@@ -479,7 +499,7 @@ describe('Selector hooks', () => {
479
499
  return stateHandler;
480
500
  });
481
501
  const renderSpy = jest.fn();
482
- const actionsReadySpy = jest.fn();
502
+ const actionsReadySpy = jest.fn<void, [TestActions]>();
483
503
 
484
504
  act(() => {
485
505
  root.render(
@@ -491,7 +511,7 @@ describe('Selector hooks', () => {
491
511
  );
492
512
  });
493
513
 
494
- const actions = actionsReadySpy.mock.calls[0][0] as TestActions;
514
+ const [[actions]] = actionsReadySpy.mock.calls as [[TestActions]];
495
515
 
496
516
  act(() => {
497
517
  actions.increment();
@@ -517,7 +537,9 @@ describe('Selector hooks', () => {
517
537
  const renderSpy = jest.fn();
518
538
 
519
539
  act(() => {
520
- root.render(<FactoryShortcutConsumer createStateHandler={createStateHandler} onRender={renderSpy} />);
540
+ root.render(
541
+ <FactoryShortcutConsumer createStateHandler={createStateHandler} onRender={renderSpy} />
542
+ );
521
543
  });
522
544
 
523
545
  act(() => {
@@ -564,6 +586,55 @@ describe('Selector hooks', () => {
564
586
  expect(renderSpy).toHaveBeenCalledTimes(2);
565
587
  });
566
588
 
589
+ it('useStateSubscription should cache object selector snapshots within one store version', () => {
590
+ let stateHandler: TestStateHandler | null = null;
591
+ const createStateHandler = jest.fn(() => {
592
+ if (!stateHandler) {
593
+ stateHandler = new TestStateHandler({
594
+ user: { name: 'Ada' },
595
+ counter: 0,
596
+ });
597
+ }
598
+
599
+ return stateHandler;
600
+ });
601
+ const renderSpy = jest.fn();
602
+
603
+ expect(() => {
604
+ act(() => {
605
+ root.render(
606
+ <ObjectSelectorConsumer createStateHandler={createStateHandler} onRender={renderSpy} />
607
+ );
608
+ });
609
+ }).not.toThrow();
610
+
611
+ expect(renderSpy).toHaveBeenCalledTimes(1);
612
+ expect(renderSpy).toHaveBeenLastCalledWith({
613
+ counter: 0,
614
+ userName: 'Ada',
615
+ });
616
+
617
+ act(() => {
618
+ stateHandler!.getActions().increment();
619
+ });
620
+
621
+ expect(renderSpy).toHaveBeenCalledTimes(2);
622
+ expect(renderSpy).toHaveBeenLastCalledWith({
623
+ counter: 1,
624
+ userName: 'Ada',
625
+ });
626
+
627
+ act(() => {
628
+ stateHandler!.getActions().setName('Grace');
629
+ });
630
+
631
+ expect(renderSpy).toHaveBeenCalledTimes(3);
632
+ expect(renderSpy).toHaveBeenLastCalledWith({
633
+ counter: 1,
634
+ userName: 'Grace',
635
+ });
636
+ });
637
+
567
638
  it('useStateSubscription should return full snapshot when no selector is provided', () => {
568
639
  let stateHandler: TestStateHandler | null = null;
569
640
  const createStateHandler = jest.fn(() => {
@@ -577,7 +648,7 @@ describe('Selector hooks', () => {
577
648
  return stateHandler;
578
649
  });
579
650
  const renderSpy = jest.fn();
580
- const actionsReadySpy = jest.fn();
651
+ const actionsReadySpy = jest.fn<void, [TestActions]>();
581
652
 
582
653
  act(() => {
583
654
  root.render(
@@ -594,7 +665,7 @@ describe('Selector hooks', () => {
594
665
  counter: 0,
595
666
  });
596
667
 
597
- const actions = actionsReadySpy.mock.calls[0][0] as TestActions;
668
+ const [[actions]] = actionsReadySpy.mock.calls as [[TestActions]];
598
669
 
599
670
  act(() => {
600
671
  actions.increment();
@@ -611,16 +682,18 @@ describe('Selector hooks', () => {
611
682
  user: { name: 'Ada' },
612
683
  counter: 0,
613
684
  });
614
- const singleton = makeStateSingleton(() => stateHandler);
685
+ const singleton = makeStateSingleton(() => stateHandler, {
686
+ destroyOnNoConsumers: true,
687
+ });
615
688
  const firstRenderSpy = jest.fn();
616
689
  const secondRenderSpy = jest.fn();
617
690
 
618
691
  act(() => {
619
692
  root.render(
620
- <>
693
+ <React.Fragment>
621
694
  <SingletonShortcutConsumer singleton={singleton} onRender={firstRenderSpy} />
622
695
  <SingletonShortcutConsumer singleton={singleton} onRender={secondRenderSpy} />
623
- </>
696
+ </React.Fragment>
624
697
  );
625
698
  });
626
699
 
@@ -645,7 +718,7 @@ describe('Selector hooks', () => {
645
718
  expect(stateHandler.destroy).not.toHaveBeenCalled();
646
719
 
647
720
  act(() => {
648
- root.render(<></>);
721
+ root.render(<React.Fragment />);
649
722
  });
650
723
 
651
724
  expect(stateHandler.destroy).toHaveBeenCalledTimes(1);
@@ -683,7 +756,7 @@ describe('Selector hooks', () => {
683
756
  });
684
757
  const singleton = makeStateSingleton(() => stateHandler);
685
758
  const renderSpy = jest.fn();
686
- const actionsReadySpy = jest.fn();
759
+ const actionsReadySpy = jest.fn<void, [TestActions]>();
687
760
 
688
761
  act(() => {
689
762
  root.render(
@@ -695,7 +768,7 @@ describe('Selector hooks', () => {
695
768
  );
696
769
  });
697
770
 
698
- const actions = actionsReadySpy.mock.calls[0][0] as TestActions;
771
+ const [[actions]] = actionsReadySpy.mock.calls as [[TestActions]];
699
772
 
700
773
  act(() => {
701
774
  actions.increment();
@@ -710,7 +783,7 @@ describe('Selector hooks', () => {
710
783
  expect(renderSpy).toHaveBeenCalledTimes(2);
711
784
  });
712
785
 
713
- it('useStateSingleton selector should respect destroyOnNoConsumers false', () => {
786
+ it('useStateSingleton selector should keep the singleton instance alive by default', () => {
714
787
  const firstHandler = new TestStateHandler({
715
788
  user: { name: 'Ada' },
716
789
  counter: 0,
@@ -723,9 +796,7 @@ describe('Selector hooks', () => {
723
796
  .fn(() => firstHandler as StateSubscriptionHandler<TestState, TestActions>)
724
797
  .mockReturnValueOnce(firstHandler)
725
798
  .mockReturnValueOnce(secondHandler);
726
- const singleton = makeStateSingleton<TestState, TestActions>(createStateHandler, {
727
- destroyOnNoConsumers: false,
728
- });
799
+ const singleton = makeStateSingleton<TestState, TestActions>(createStateHandler);
729
800
  const renderSpy = jest.fn();
730
801
 
731
802
  act(() => {
@@ -733,7 +804,7 @@ describe('Selector hooks', () => {
733
804
  });
734
805
 
735
806
  act(() => {
736
- root.render(<></>);
807
+ root.render(<React.Fragment />);
737
808
  });
738
809
 
739
810
  expect(firstHandler.destroy).not.toHaveBeenCalled();
@@ -764,7 +835,10 @@ describe('Selector hooks', () => {
764
835
  act(() => {
765
836
  root.render(
766
837
  <React.StrictMode>
767
- <StrictModeMirrorFactoryConsumer createStateHandler={createStateHandler} onRender={renderSpy} />
838
+ <StrictModeMirrorFactoryConsumer
839
+ createStateHandler={createStateHandler}
840
+ onRender={renderSpy}
841
+ />
768
842
  </React.StrictMode>
769
843
  );
770
844
  });