@veams/status-quo 1.10.0 → 1.12.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/.turbo/turbo-build.log +14 -12
  2. package/CHANGELOG.md +2 -0
  3. package/README.md +43 -5
  4. package/dist/index.d.ts +2 -2
  5. package/dist/index.js +2 -6
  6. package/dist/index.js.map +1 -1
  7. package/dist/observable/index.d.ts +5 -0
  8. package/dist/observable/index.js +6 -0
  9. package/dist/observable/index.js.map +1 -0
  10. package/dist/react/hooks/__tests__/state-selector.spec.js +103 -0
  11. package/dist/react/hooks/__tests__/state-selector.spec.js.map +1 -1
  12. package/dist/react/hooks/state-subscription-selector.js +46 -34
  13. package/dist/react/hooks/state-subscription-selector.js.map +1 -1
  14. package/dist/signals/index.d.ts +5 -0
  15. package/dist/signals/index.js +6 -0
  16. package/dist/signals/index.js.map +1 -0
  17. package/dist/store/__tests__/native-state-handler.spec.js +62 -0
  18. package/dist/store/__tests__/native-state-handler.spec.js.map +1 -1
  19. package/dist/store/base-state-handler.d.ts +22 -0
  20. package/dist/store/base-state-handler.js +76 -0
  21. package/dist/store/base-state-handler.js.map +1 -1
  22. package/dist/store/index.d.ts +0 -2
  23. package/dist/store/index.js +0 -4
  24. package/dist/store/index.js.map +1 -1
  25. package/dist/types/types.d.ts +2 -0
  26. package/package.json +15 -1
  27. package/src/index.ts +0 -6
  28. package/src/observable/index.ts +5 -0
  29. package/src/react/hooks/__tests__/state-selector.spec.tsx +160 -1
  30. package/src/react/hooks/state-subscription-selector.tsx +54 -40
  31. package/src/signals/index.ts +5 -0
  32. package/src/store/__tests__/native-state-handler.spec.ts +80 -0
  33. package/src/store/base-state-handler.ts +88 -0
  34. package/src/store/index.ts +0 -4
  35. package/src/types/types.ts +4 -0
  36. package/dist/hooks/__tests__/state-provider.spec.d.ts +0 -4
  37. package/dist/hooks/__tests__/state-provider.spec.js +0 -179
  38. package/dist/hooks/__tests__/state-provider.spec.js.map +0 -1
  39. package/dist/hooks/__tests__/state-selector.spec.d.ts +0 -4
  40. package/dist/hooks/__tests__/state-selector.spec.js +0 -499
  41. package/dist/hooks/__tests__/state-selector.spec.js.map +0 -1
  42. package/dist/hooks/__tests__/state-singleton.spec.d.ts +0 -4
  43. package/dist/hooks/__tests__/state-singleton.spec.js +0 -96
  44. package/dist/hooks/__tests__/state-singleton.spec.js.map +0 -1
  45. package/dist/hooks/index.d.ts +0 -6
  46. package/dist/hooks/index.js +0 -7
  47. package/dist/hooks/index.js.map +0 -1
  48. package/dist/hooks/state-actions.d.ts +0 -2
  49. package/dist/hooks/state-actions.js +0 -5
  50. package/dist/hooks/state-actions.js.map +0 -1
  51. package/dist/hooks/state-factory.d.ts +0 -7
  52. package/dist/hooks/state-factory.js +0 -13
  53. package/dist/hooks/state-factory.js.map +0 -1
  54. package/dist/hooks/state-handler.d.ts +0 -2
  55. package/dist/hooks/state-handler.js +0 -9
  56. package/dist/hooks/state-handler.js.map +0 -1
  57. package/dist/hooks/state-provider.d.ts +0 -14
  58. package/dist/hooks/state-provider.js +0 -24
  59. package/dist/hooks/state-provider.js.map +0 -1
  60. package/dist/hooks/state-singleton.d.ts +0 -6
  61. package/dist/hooks/state-singleton.js +0 -7
  62. package/dist/hooks/state-singleton.js.map +0 -1
  63. package/dist/hooks/state-subscription-selector.d.ts +0 -3
  64. package/dist/hooks/state-subscription-selector.js +0 -63
  65. package/dist/hooks/state-subscription-selector.js.map +0 -1
  66. package/dist/hooks/state-subscription.d.ts +0 -9
  67. package/dist/hooks/state-subscription.js +0 -53
  68. package/dist/hooks/state-subscription.js.map +0 -1
@@ -20,12 +20,12 @@ type Listener = () => void;
20
20
  type SharedStateSubscriptionHandler = StateSubscriptionHandler<unknown, unknown>;
21
21
 
22
22
  /**
23
- * Tracks the reference count and deferred destruction status of a state handler.
23
+ * Tracks the reference count and deferred lifecycle cleanup status of a state handler.
24
24
  */
25
- type DeferredDestroy = {
25
+ type DeferredLifecycle = {
26
26
  // Number of active consumers of the state handler.
27
27
  refCount: number;
28
- // ID of the timeout for deferred destruction.
28
+ // ID of the timeout for deferred disconnect/destruction.
29
29
  timeoutId: ReturnType<typeof setTimeout> | null;
30
30
  };
31
31
 
@@ -51,18 +51,18 @@ type ServerSnapshotCacheEntry<Source, Selected> = {
51
51
  sourceSnapshot: Source;
52
52
  };
53
53
 
54
- // Global map to track deferred destruction status for each state handler instance.
55
- const deferredDestroyMap = new WeakMap<SharedStateSubscriptionHandler, DeferredDestroy>();
54
+ // Global map to track deferred lifecycle status for each state handler instance.
55
+ const deferredLifecycleMap = new WeakMap<SharedStateSubscriptionHandler, DeferredLifecycle>();
56
56
 
57
57
  /**
58
- * Returns the deferred destruction status for a given state handler instance.
58
+ * Returns the deferred lifecycle status for a given state handler instance.
59
59
  * Initializes the status if it does not already exist.
60
60
  */
61
- function getDeferredDestroyState(
61
+ function getDeferredLifecycleState(
62
62
  stateSubscriptionHandler: SharedStateSubscriptionHandler
63
- ): DeferredDestroy {
63
+ ): DeferredLifecycle {
64
64
  // Retrieve the existing status from the map.
65
- const existingState = deferredDestroyMap.get(stateSubscriptionHandler);
65
+ const existingState = deferredLifecycleMap.get(stateSubscriptionHandler);
66
66
 
67
67
  // If status already exists, return it.
68
68
  if (existingState) {
@@ -70,12 +70,12 @@ function getDeferredDestroyState(
70
70
  }
71
71
 
72
72
  // Create and store a new status for the handler.
73
- const nextState: DeferredDestroy = {
73
+ const nextState: DeferredLifecycle = {
74
74
  refCount: 0,
75
75
  timeoutId: null,
76
76
  };
77
77
 
78
- deferredDestroyMap.set(stateSubscriptionHandler, nextState);
78
+ deferredLifecycleMap.set(stateSubscriptionHandler, nextState);
79
79
 
80
80
  return nextState;
81
81
  }
@@ -113,17 +113,20 @@ export function useStateSubscriptionSelector<V, A, Sel>(
113
113
  // Subscription function to be used by useSyncExternalStore.
114
114
  const subscribe = useCallback(
115
115
  (listener: Listener) => {
116
- // Access the deferred destruction status for this handler.
116
+ // Access the deferred lifecycle status for this handler.
117
117
  const sharedStateSubscriptionHandler =
118
118
  stateSubscriptionHandler as unknown as SharedStateSubscriptionHandler;
119
- const deferredDestroyState = getDeferredDestroyState(sharedStateSubscriptionHandler);
119
+ const deferredLifecycleState = getDeferredLifecycleState(sharedStateSubscriptionHandler);
120
+ const hadPendingCleanup = deferredLifecycleState.timeoutId !== null;
121
+ const wasIdle = deferredLifecycleState.refCount === 0;
122
+
120
123
  // Increment the consumer reference count.
121
- deferredDestroyState.refCount += 1;
124
+ deferredLifecycleState.refCount += 1;
122
125
 
123
- // If a pending destruction timeout is scheduled, cancel it.
124
- if (deferredDestroyState.timeoutId) {
125
- clearTimeout(deferredDestroyState.timeoutId);
126
- deferredDestroyState.timeoutId = null;
126
+ // If a pending cleanup timeout is scheduled, cancel it.
127
+ if (deferredLifecycleState.timeoutId) {
128
+ clearTimeout(deferredLifecycleState.timeoutId);
129
+ deferredLifecycleState.timeoutId = null;
127
130
  }
128
131
 
129
132
  // Subscribe to the state handler.
@@ -135,51 +138,62 @@ export function useStateSubscriptionSelector<V, A, Sel>(
135
138
  listener();
136
139
  });
137
140
 
141
+ if (wasIdle && !hadPendingCleanup) {
142
+ stateSubscriptionHandler.connect?.();
143
+ }
144
+
138
145
  // Return an unsubscribe function to be called by React.
139
146
  return () => {
140
147
  // Execute the handler's unsubscribe method.
141
148
  unsubscribe();
142
149
 
143
- // If automatic cleanup is disabled, stop here.
144
- if (!destroyOnCleanup) {
145
- return;
146
- }
147
-
148
- // Retrieve the current destruction status.
149
- const activeDeferredDestroyState = deferredDestroyMap.get(sharedStateSubscriptionHandler);
150
+ // Retrieve the current lifecycle status.
151
+ const activeDeferredLifecycleState = deferredLifecycleMap.get(
152
+ sharedStateSubscriptionHandler
153
+ );
150
154
 
151
155
  // If no status is found, stop here.
152
- if (!activeDeferredDestroyState) {
156
+ if (!activeDeferredLifecycleState) {
153
157
  return;
154
158
  }
155
159
 
156
160
  // Decrement the consumer reference count.
157
- activeDeferredDestroyState.refCount -= 1;
161
+ activeDeferredLifecycleState.refCount -= 1;
158
162
 
159
- // If there are still active consumers, do not destroy the handler.
160
- if (activeDeferredDestroyState.refCount > 0) {
163
+ // If there are still active consumers, keep the handler connected.
164
+ if (activeDeferredLifecycleState.refCount > 0) {
161
165
  return;
162
166
  }
163
167
 
164
168
  // Reset the reference count to zero.
165
- activeDeferredDestroyState.refCount = 0;
166
- // Schedule deferred destruction to allow for potential immediate re-subscriptions.
167
- activeDeferredDestroyState.timeoutId = setTimeout(() => {
169
+ activeDeferredLifecycleState.refCount = 0;
170
+ // Schedule deferred cleanup to allow for potential immediate re-subscriptions.
171
+ activeDeferredLifecycleState.timeoutId = setTimeout(() => {
168
172
  // Check if the handler still has no consumers after the timeout.
169
- const pendingDeferredDestroyState = deferredDestroyMap.get(
173
+ const pendingDeferredLifecycleState = deferredLifecycleMap.get(
170
174
  sharedStateSubscriptionHandler
171
175
  );
172
176
 
173
- // If consumers have reappeared, do not destroy the handler.
174
- if (!pendingDeferredDestroyState || pendingDeferredDestroyState.refCount > 0) {
177
+ // If consumers have reappeared, keep the handler connected.
178
+ if (!pendingDeferredLifecycleState || pendingDeferredLifecycleState.refCount > 0) {
175
179
  return;
176
180
  }
177
181
 
178
- // Clear the pending timeout and destroy the state handler.
179
- pendingDeferredDestroyState.timeoutId = null;
180
- stateSubscriptionHandler.destroy();
181
- // Remove the status from the global map.
182
- deferredDestroyMap.delete(sharedStateSubscriptionHandler);
182
+ // Clear the pending timeout and disconnect the state handler.
183
+ pendingDeferredLifecycleState.timeoutId = null;
184
+
185
+ try {
186
+ stateSubscriptionHandler.disconnect?.();
187
+ } finally {
188
+ if (destroyOnCleanup) {
189
+ try {
190
+ stateSubscriptionHandler.destroy();
191
+ } finally {
192
+ // Remove the status from the global map after final destruction.
193
+ deferredLifecycleMap.delete(sharedStateSubscriptionHandler);
194
+ }
195
+ }
196
+ }
183
197
  }, 0);
184
198
  };
185
199
  },
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Subpath entry for the Preact Signals-based SignalStateHandler.
3
+ * Import from '@veams/status-quo/signals' to avoid pulling @preact/signals-core into the main bundle.
4
+ */
5
+ export { SignalStateHandler } from '../store/signal-state-handler.js';
@@ -51,6 +51,7 @@ type CounterBucketSelection = { bucket: number };
51
51
  type CounterBucketState = { bucket: number };
52
52
  type SetState = { openItems: Set<string> };
53
53
  type SetActions = { toggle: (id: string) => void };
54
+ type NoopActions = { noop: () => void };
54
55
  type CounterSubscribable = {
55
56
  subscribe: (listener: (value: CounterState) => void) => () => void;
56
57
  getSnapshot: () => CounterState;
@@ -200,6 +201,37 @@ class SetNativeStateHandler extends NativeStateHandler<SetState, SetActions> {
200
201
  }
201
202
  }
202
203
 
204
+ class LifecycleNativeStateHandler extends NativeStateHandler<CounterState, NoopActions> {
205
+ constructor(
206
+ private readonly onConnectSpy: () => void,
207
+ private readonly onDisconnectSpy: () => void
208
+ ) {
209
+ super({
210
+ initialState: {
211
+ count: 0,
212
+ },
213
+ });
214
+ }
215
+
216
+ trackSubscription(subscription: { unsubscribe: () => void }) {
217
+ this.subscriptions = [...this.subscriptions, subscription];
218
+ }
219
+
220
+ protected override onConnect(): void {
221
+ this.onConnectSpy();
222
+ }
223
+
224
+ protected override onDisconnect(): void {
225
+ this.onDisconnectSpy();
226
+ }
227
+
228
+ getActions(): NoopActions {
229
+ return {
230
+ noop: () => undefined,
231
+ };
232
+ }
233
+ }
234
+
203
235
  function createCounterSubscribable(initialCount: number) {
204
236
  let listener: ((value: CounterState) => void) | null = null;
205
237
  const unsubscribe = jest.fn(() => {
@@ -269,6 +301,54 @@ describe('Native State Handler', () => {
269
301
  expect(spy).toHaveBeenCalledTimes(1);
270
302
  });
271
303
 
304
+ it('should connect and disconnect side effects with reference counting', () => {
305
+ const onConnectSpy = jest.fn();
306
+ const onDisconnectSpy = jest.fn();
307
+ const handler = new LifecycleNativeStateHandler(onConnectSpy, onDisconnectSpy);
308
+
309
+ expect(handler.getSnapshot()).toStrictEqual({ count: 0 });
310
+ expect(onConnectSpy).not.toHaveBeenCalled();
311
+
312
+ handler.connect();
313
+ handler.connect();
314
+ handler.disconnect();
315
+
316
+ expect(onConnectSpy).toHaveBeenCalledTimes(1);
317
+ expect(onDisconnectSpy).not.toHaveBeenCalled();
318
+
319
+ handler.disconnect();
320
+
321
+ expect(onDisconnectSpy).toHaveBeenCalledTimes(1);
322
+ });
323
+
324
+ it('should clear managed subscriptions on disconnect', () => {
325
+ const handler = new LifecycleNativeStateHandler(jest.fn(), jest.fn());
326
+ const unsubscribeSpy = jest.fn();
327
+
328
+ handler.trackSubscription({ unsubscribe: unsubscribeSpy });
329
+ handler.connect();
330
+ handler.disconnect();
331
+
332
+ expect(unsubscribeSpy).toHaveBeenCalledTimes(1);
333
+ expect(handler.subscriptions).toStrictEqual([]);
334
+ });
335
+
336
+ it('should disconnect and clear managed subscriptions on destroy', () => {
337
+ const onDisconnectSpy = jest.fn();
338
+ const handler = new LifecycleNativeStateHandler(jest.fn(), onDisconnectSpy);
339
+ const unsubscribeSpy = jest.fn();
340
+
341
+ handler.trackSubscription({ unsubscribe: unsubscribeSpy });
342
+ handler.connect();
343
+ handler.connect();
344
+
345
+ handler.destroy();
346
+ handler.disconnect();
347
+
348
+ expect(onDisconnectSpy).toHaveBeenCalledTimes(1);
349
+ expect(unsubscribeSpy).toHaveBeenCalledTimes(1);
350
+ });
351
+
272
352
  it('should call subscriber when state has changed and also on initial subscribe', () => {
273
353
  const spy = jest.fn();
274
354
  const unsubscribe = stateHandler.subscribe(spy);
@@ -53,6 +53,11 @@ export abstract class BaseStateHandler<S, A> implements StateSubscriptionHandler
53
53
  // Holds the Redux DevTools instance if enabled.
54
54
  protected devTools: DevTools | null = null;
55
55
 
56
+ // Tracks mounted consumers that have connected this handler.
57
+ private connectCount = 0;
58
+ // Prevents duplicate side effects while the handler is already connected.
59
+ private isConnected = false;
60
+
56
61
  // Keeps track of active subscriptions to allow for cleanup.
57
62
  subscriptions: ManagedSubscription[] = [];
58
63
  // Tracks keyed subscriptions so handlers can replace them by name.
@@ -127,10 +132,93 @@ export abstract class BaseStateHandler<S, A> implements StateSubscriptionHandler
127
132
  this.devTools?.send(actionName, nextState);
128
133
  }
129
134
 
135
+ /**
136
+ * Starts external effects for this handler.
137
+ */
138
+ connect(): void {
139
+ const previousConnectCount = this.connectCount;
140
+ this.connectCount += 1;
141
+
142
+ if (this.isConnected) {
143
+ return;
144
+ }
145
+
146
+ this.isConnected = true;
147
+
148
+ try {
149
+ this.onConnect();
150
+ } catch (error) {
151
+ this.connectCount = previousConnectCount;
152
+ this.isConnected = false;
153
+ this.clearManagedSubscriptions();
154
+ throw error;
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Stops external effects once all connected consumers have disconnected.
160
+ */
161
+ disconnect(): void {
162
+ if (this.connectCount === 0) {
163
+ return;
164
+ }
165
+
166
+ this.connectCount -= 1;
167
+
168
+ if (this.connectCount > 0) {
169
+ return;
170
+ }
171
+
172
+ if (!this.isConnected) {
173
+ this.clearManagedSubscriptions();
174
+ return;
175
+ }
176
+
177
+ try {
178
+ this.onDisconnect();
179
+ } finally {
180
+ this.isConnected = false;
181
+ this.clearManagedSubscriptions();
182
+ }
183
+ }
184
+
130
185
  /**
131
186
  * Cleans up all active subscriptions when the handler is destroyed.
132
187
  */
133
188
  destroy(): void {
189
+ this.connectCount = 0;
190
+
191
+ if (!this.isConnected) {
192
+ this.clearManagedSubscriptions();
193
+ return;
194
+ }
195
+
196
+ try {
197
+ this.onDisconnect();
198
+ } finally {
199
+ this.isConnected = false;
200
+ this.clearManagedSubscriptions();
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Optional hook for subclasses that need to start external effects.
206
+ */
207
+ protected onConnect(): void {
208
+ // Optional override.
209
+ }
210
+
211
+ /**
212
+ * Optional hook for subclasses that need to stop external effects.
213
+ */
214
+ protected onDisconnect(): void {
215
+ // Optional override.
216
+ }
217
+
218
+ /**
219
+ * Clears all subscriptions managed through this base handler.
220
+ */
221
+ protected clearManagedSubscriptions(): void {
134
222
  const subscriptions = [...this.subscriptions];
135
223
  const namedSubscriptions = [...this.namedSubscriptions.values()];
136
224
 
@@ -6,10 +6,6 @@
6
6
  export { BaseStateHandler } from './base-state-handler.js';
7
7
  // Export the lightweight state handler using plain JavaScript.
8
8
  export { NativeStateHandler } from './native-state-handler.js';
9
- // Export the state handler powered by RxJS BehaviorSubjects.
10
- export { ObservableStateHandler } from './observable-state-handler.js';
11
- // Export the state handler powered by Preact Signals.
12
- export { SignalStateHandler } from './signal-state-handler.js';
13
9
 
14
10
  /**
15
11
  * Export singleton related types and factory function.
@@ -13,6 +13,10 @@ export interface StateSubscriptionHandler<V, A> {
13
13
  subscribe(listener: () => void): () => void;
14
14
  // Method to subscribe a listener that receives the updated state value.
15
15
  subscribe(listener: (value: V) => void): () => void;
16
+ // Optional method to start external effects after a committed consumer subscribes.
17
+ connect?: () => void;
18
+ // Optional method to stop external effects after the last consumer unsubscribes.
19
+ disconnect?: () => void;
16
20
  // Method to retrieve the current state snapshot.
17
21
  getSnapshot: () => V;
18
22
  // Method to clean up resources associated with the handler.
@@ -1,4 +0,0 @@
1
- declare global {
2
- var IS_REACT_ACT_ENVIRONMENT: boolean;
3
- }
4
- export {};
@@ -1,179 +0,0 @@
1
- import React, { act } from 'react';
2
- import { createRoot } from 'react-dom/client';
3
- import { StateProvider, useProvidedStateActions, useProvidedStateHandler, useProvidedStateSubscription, } from '../state-provider.js';
4
- class TestStateHandler {
5
- initialState;
6
- state;
7
- listeners = new Set();
8
- destroy = jest.fn();
9
- constructor(initialState) {
10
- this.initialState = initialState;
11
- this.state = initialState;
12
- }
13
- subscribe(listener) {
14
- const typedListener = listener;
15
- this.listeners.add(typedListener);
16
- return () => {
17
- this.listeners.delete(typedListener);
18
- };
19
- }
20
- getSnapshot = () => {
21
- return this.state;
22
- };
23
- getInitialState = () => {
24
- return this.initialState;
25
- };
26
- getActions = () => {
27
- return {
28
- increment: () => {
29
- this.state = {
30
- ...this.state,
31
- count: this.state.count + 1,
32
- };
33
- this.emitStateChange();
34
- },
35
- rename: (label) => {
36
- this.state = {
37
- ...this.state,
38
- label,
39
- };
40
- this.emitStateChange();
41
- },
42
- };
43
- };
44
- emitStateChange() {
45
- const nextState = this.state;
46
- this.listeners.forEach((listener) => listener(nextState));
47
- }
48
- }
49
- function CountConsumer({ onRender }) {
50
- const [count] = useProvidedStateSubscription((state) => state.count);
51
- onRender(count);
52
- return React.createElement("span", null, count);
53
- }
54
- function FullStateConsumer({ onActionsReady, onRender, }) {
55
- const [state, actions] = useProvidedStateSubscription();
56
- onRender(state);
57
- onActionsReady(actions);
58
- return React.createElement("span", null, state.label);
59
- }
60
- function ActionsOnlyConsumer({ onActionsReady, onRender, }) {
61
- const actions = useProvidedStateActions();
62
- onRender();
63
- onActionsReady(actions);
64
- return React.createElement("span", null, "actions-only");
65
- }
66
- function HandlerConsumer({ onHandlerReady, }) {
67
- const handler = useProvidedStateHandler();
68
- onHandlerReady(handler);
69
- return React.createElement("span", null, "handler");
70
- }
71
- function MissingProviderConsumer() {
72
- useProvidedStateActions();
73
- return null;
74
- }
75
- describe('StateProvider', () => {
76
- let container;
77
- let root;
78
- beforeAll(() => {
79
- globalThis.IS_REACT_ACT_ENVIRONMENT = true;
80
- });
81
- beforeEach(() => {
82
- container = document.createElement('div');
83
- document.body.appendChild(container);
84
- root = createRoot(container);
85
- });
86
- afterEach(() => {
87
- act(() => {
88
- root.unmount();
89
- });
90
- container.remove();
91
- });
92
- afterAll(() => {
93
- globalThis.IS_REACT_ACT_ENVIRONMENT = false;
94
- });
95
- it('should share one handler instance across the provider subtree', () => {
96
- const stateHandler = new TestStateHandler({
97
- count: 0,
98
- label: 'Counter',
99
- });
100
- const countRenderSpy = jest.fn();
101
- const actionsRenderSpy = jest.fn();
102
- const actionsReadySpy = jest.fn();
103
- const handlerReadySpy = jest.fn();
104
- act(() => {
105
- root.render(React.createElement(StateProvider, { instance: stateHandler },
106
- React.createElement(CountConsumer, { onRender: countRenderSpy }),
107
- React.createElement(ActionsOnlyConsumer, { onActionsReady: actionsReadySpy, onRender: actionsRenderSpy }),
108
- React.createElement(HandlerConsumer, { onHandlerReady: handlerReadySpy })));
109
- });
110
- expect(countRenderSpy).toHaveBeenCalledTimes(1);
111
- expect(countRenderSpy).toHaveBeenLastCalledWith(0);
112
- expect(actionsRenderSpy).toHaveBeenCalledTimes(1);
113
- expect(handlerReadySpy).toHaveBeenCalledWith(stateHandler);
114
- const [[actions]] = actionsReadySpy.mock.calls;
115
- act(() => {
116
- actions.rename('Renamed');
117
- });
118
- expect(countRenderSpy).toHaveBeenCalledTimes(1);
119
- expect(actionsRenderSpy).toHaveBeenCalledTimes(1);
120
- act(() => {
121
- actions.increment();
122
- });
123
- expect(countRenderSpy).toHaveBeenCalledTimes(2);
124
- expect(countRenderSpy).toHaveBeenLastCalledWith(1);
125
- expect(actionsRenderSpy).toHaveBeenCalledTimes(1);
126
- });
127
- it('should return the full snapshot when no selector is provided', () => {
128
- const stateHandler = new TestStateHandler({
129
- count: 2,
130
- label: 'Counter',
131
- });
132
- const renderSpy = jest.fn();
133
- const actionsReadySpy = jest.fn();
134
- act(() => {
135
- root.render(React.createElement(StateProvider, { instance: stateHandler },
136
- React.createElement(FullStateConsumer, { onActionsReady: actionsReadySpy, onRender: renderSpy })));
137
- });
138
- expect(renderSpy).toHaveBeenCalledTimes(1);
139
- expect(renderSpy).toHaveBeenLastCalledWith({ count: 2, label: 'Counter' });
140
- const [[actions]] = actionsReadySpy.mock.calls;
141
- act(() => {
142
- actions.increment();
143
- });
144
- expect(renderSpy).toHaveBeenCalledTimes(2);
145
- expect(renderSpy).toHaveBeenLastCalledWith({ count: 3, label: 'Counter' });
146
- });
147
- it('should follow a new instance when the provider instance changes', () => {
148
- const firstHandler = new TestStateHandler({
149
- count: 1,
150
- label: 'First',
151
- });
152
- const secondHandler = new TestStateHandler({
153
- count: 8,
154
- label: 'Second',
155
- });
156
- const renderSpy = jest.fn();
157
- act(() => {
158
- root.render(React.createElement(StateProvider, { instance: firstHandler },
159
- React.createElement(CountConsumer, { onRender: renderSpy })));
160
- });
161
- act(() => {
162
- root.render(React.createElement(StateProvider, { instance: secondHandler },
163
- React.createElement(CountConsumer, { onRender: renderSpy })));
164
- });
165
- expect(renderSpy).toHaveBeenCalledTimes(2);
166
- expect(renderSpy).toHaveBeenNthCalledWith(1, 1);
167
- expect(renderSpy).toHaveBeenNthCalledWith(2, 8);
168
- });
169
- it('should throw when provider hooks are used outside StateProvider', () => {
170
- const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined);
171
- expect(() => {
172
- act(() => {
173
- root.render(React.createElement(MissingProviderConsumer, null));
174
- });
175
- }).toThrow('No StateProvider instance found in the current React tree.');
176
- consoleErrorSpy.mockRestore();
177
- });
178
- });
179
- //# sourceMappingURL=state-provider.spec.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"state-provider.spec.js","sourceRoot":"","sources":["../../../src/hooks/__tests__/state-provider.spec.tsx"],"names":[],"mappings":"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"}
@@ -1,4 +0,0 @@
1
- declare global {
2
- var IS_REACT_ACT_ENVIRONMENT: boolean;
3
- }
4
- export {};