@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.
- package/.turbo/turbo-build.log +12 -0
- package/.turbo/turbo-check$colon$types.log +4 -0
- package/.turbo/turbo-docs$colon$build.log +14 -0
- package/.turbo/turbo-lint.log +8 -0
- package/.turbo/turbo-test.log +15 -0
- package/CHANGELOG.md +24 -3
- package/README.md +217 -41
- package/dist/config/status-quo-config.d.ts +13 -0
- package/dist/config/status-quo-config.js +14 -0
- package/dist/config/status-quo-config.js.map +1 -1
- package/dist/hooks/__tests__/state-provider.spec.d.ts +4 -0
- package/dist/hooks/__tests__/state-provider.spec.js +179 -0
- package/dist/hooks/__tests__/state-provider.spec.js.map +1 -0
- package/dist/hooks/__tests__/state-selector.spec.js +11 -12
- package/dist/hooks/__tests__/state-selector.spec.js.map +1 -1
- package/dist/hooks/__tests__/state-singleton.spec.js +10 -11
- package/dist/hooks/__tests__/state-singleton.spec.js.map +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/state-factory.js.map +1 -1
- package/dist/hooks/state-provider.d.ts +14 -0
- package/dist/hooks/state-provider.js +24 -0
- package/dist/hooks/state-provider.js.map +1 -0
- package/dist/hooks/state-subscription-selector.js +6 -2
- package/dist/hooks/state-subscription-selector.js.map +1 -1
- package/dist/hooks/state-subscription.js +1 -1
- package/dist/hooks/state-subscription.js.map +1 -1
- package/dist/index.d.ts +4 -5
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/dist/react/hooks/__tests__/state-provider.spec.d.ts +4 -0
- package/dist/react/hooks/__tests__/state-provider.spec.js +179 -0
- package/dist/react/hooks/__tests__/state-provider.spec.js.map +1 -0
- package/dist/react/hooks/__tests__/state-selector.spec.d.ts +4 -0
- package/dist/react/hooks/__tests__/state-selector.spec.js +547 -0
- package/dist/react/hooks/__tests__/state-selector.spec.js.map +1 -0
- package/dist/react/hooks/__tests__/state-singleton.spec.d.ts +4 -0
- package/dist/react/hooks/__tests__/state-singleton.spec.js +96 -0
- package/dist/react/hooks/__tests__/state-singleton.spec.js.map +1 -0
- package/{src/hooks/index.ts → dist/react/hooks/index.d.ts} +1 -0
- package/dist/react/hooks/index.js +7 -0
- package/dist/react/hooks/index.js.map +1 -0
- package/dist/react/hooks/state-actions.d.ts +2 -0
- package/dist/react/hooks/state-actions.js +5 -0
- package/dist/react/hooks/state-actions.js.map +1 -0
- package/dist/react/hooks/state-factory.d.ts +7 -0
- package/dist/react/hooks/state-factory.js +13 -0
- package/dist/react/hooks/state-factory.js.map +1 -0
- package/dist/react/hooks/state-handler.d.ts +2 -0
- package/dist/react/hooks/state-handler.js +9 -0
- package/dist/react/hooks/state-handler.js.map +1 -0
- package/dist/react/hooks/state-provider.d.ts +14 -0
- package/dist/react/hooks/state-provider.js +24 -0
- package/dist/react/hooks/state-provider.js.map +1 -0
- package/dist/react/hooks/state-singleton.d.ts +6 -0
- package/dist/react/hooks/state-singleton.js +7 -0
- package/dist/react/hooks/state-singleton.js.map +1 -0
- package/dist/react/hooks/state-subscription-selector.d.ts +3 -0
- package/dist/react/hooks/state-subscription-selector.js +114 -0
- package/dist/react/hooks/state-subscription-selector.js.map +1 -0
- package/dist/react/hooks/state-subscription.d.ts +9 -0
- package/dist/react/hooks/state-subscription.js +53 -0
- package/dist/react/hooks/state-subscription.js.map +1 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +2 -0
- package/dist/react/index.js.map +1 -0
- package/dist/store/__tests__/observable-state-handler.spec.js +66 -11
- package/dist/store/__tests__/observable-state-handler.spec.js.map +1 -1
- package/dist/store/__tests__/signal-state-handler.spec.js.map +1 -1
- package/dist/store/base-state-handler.d.ts +3 -5
- package/dist/store/base-state-handler.js +10 -9
- package/dist/store/base-state-handler.js.map +1 -1
- package/dist/store/dev-tools.js +0 -3
- package/dist/store/dev-tools.js.map +1 -1
- package/dist/store/observable-state-handler.d.ts +4 -10
- package/dist/store/observable-state-handler.js +4 -11
- package/dist/store/observable-state-handler.js.map +1 -1
- package/dist/store/signal-state-handler.d.ts +2 -5
- package/dist/store/signal-state-handler.js +3 -2
- package/dist/store/signal-state-handler.js.map +1 -1
- package/dist/store/state-singleton.js +1 -1
- package/dist/store/state-singleton.js.map +1 -1
- package/eslint.config.mjs +75 -0
- package/package.json +18 -18
- package/src/config/status-quo-config.ts +31 -1
- package/src/index.ts +11 -15
- package/src/react/hooks/__tests__/state-provider.spec.tsx +286 -0
- package/src/{hooks → react/hooks}/__tests__/state-selector.spec.tsx +118 -44
- package/src/{hooks → react/hooks}/__tests__/state-singleton.spec.tsx +21 -20
- package/src/react/hooks/index.ts +11 -0
- package/src/{hooks → react/hooks}/state-actions.tsx +1 -1
- package/src/{hooks → react/hooks}/state-factory.tsx +2 -2
- package/src/{hooks → react/hooks}/state-handler.tsx +1 -1
- package/src/react/hooks/state-provider.tsx +56 -0
- package/src/{hooks → react/hooks}/state-singleton.tsx +1 -1
- package/src/react/hooks/state-subscription-selector.tsx +190 -0
- package/src/{hooks → react/hooks}/state-subscription.tsx +5 -9
- package/src/react/index.ts +1 -0
- package/src/store/__tests__/observable-state-handler.spec.ts +92 -13
- package/src/store/__tests__/signal-state-handler.spec.ts +5 -1
- package/src/store/base-state-handler.ts +17 -22
- package/src/store/dev-tools.ts +3 -3
- package/src/store/observable-state-handler.ts +12 -22
- package/src/store/signal-state-handler.ts +11 -8
- package/src/store/state-singleton.ts +1 -1
- package/tsconfig.json +2 -3
- package/.eslintrc.cjs +0 -132
- package/.github/workflows/pages.yml +0 -46
- package/.github/workflows/release.yml +0 -33
- package/.nvmrc +0 -1
- package/.prettierrc +0 -7
- package/docs/assets/index-BBmpszOW.css +0 -1
- package/docs/assets/index-Cf8El_RO.js +0 -194
- package/docs/assets/statusquo-logo-8GVRbxpc.png +0 -0
- package/docs/index.html +0 -13
- package/playground/index.html +0 -12
- package/playground/src/App.tsx +0 -699
- package/playground/src/assets/philosophy-agnostic.svg +0 -18
- package/playground/src/assets/philosophy-separation.svg +0 -13
- package/playground/src/assets/philosophy-swap.svg +0 -17
- package/playground/src/assets/statusquo-logo.png +0 -0
- package/playground/src/main.tsx +0 -19
- package/playground/src/styles.css +0 -534
- package/playground/tsconfig.json +0 -12
- package/playground/vite.config.ts +0 -18
- 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 '
|
|
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 '
|
|
14
|
-
import type { StateSubscriptionHandler } from '
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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 = ({
|
|
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 = ({
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
838
|
+
<StrictModeMirrorFactoryConsumer
|
|
839
|
+
createStateHandler={createStateHandler}
|
|
840
|
+
onRender={renderSpy}
|
|
841
|
+
/>
|
|
768
842
|
</React.StrictMode>
|
|
769
843
|
);
|
|
770
844
|
});
|