@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.
- 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 +499 -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 +63 -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 +4 -6
- 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 +52 -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/{hooks → react/hooks}/state-subscription-selector.tsx +15 -6
- 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 -24
- 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
|
@@ -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>;
|
|
@@ -409,7 +412,7 @@ describe('Selector hooks', () => {
|
|
|
409
412
|
expect(createStateHandler).toHaveBeenCalledTimes(1);
|
|
410
413
|
|
|
411
414
|
act(() => {
|
|
412
|
-
root.render(
|
|
415
|
+
root.render(<React.Fragment />);
|
|
413
416
|
});
|
|
414
417
|
|
|
415
418
|
await act(async () => {
|
|
@@ -434,7 +437,7 @@ describe('Selector hooks', () => {
|
|
|
434
437
|
return stateHandler;
|
|
435
438
|
});
|
|
436
439
|
const stateRenderSpy = jest.fn();
|
|
437
|
-
const actionsReadySpy = jest.fn();
|
|
440
|
+
const actionsReadySpy = jest.fn<void, [TestActions]>();
|
|
438
441
|
|
|
439
442
|
act(() => {
|
|
440
443
|
root.render(
|
|
@@ -479,7 +482,7 @@ describe('Selector hooks', () => {
|
|
|
479
482
|
return stateHandler;
|
|
480
483
|
});
|
|
481
484
|
const renderSpy = jest.fn();
|
|
482
|
-
const actionsReadySpy = jest.fn();
|
|
485
|
+
const actionsReadySpy = jest.fn<void, [TestActions]>();
|
|
483
486
|
|
|
484
487
|
act(() => {
|
|
485
488
|
root.render(
|
|
@@ -491,7 +494,7 @@ describe('Selector hooks', () => {
|
|
|
491
494
|
);
|
|
492
495
|
});
|
|
493
496
|
|
|
494
|
-
const actions = actionsReadySpy.mock.calls
|
|
497
|
+
const [[actions]] = actionsReadySpy.mock.calls as [[TestActions]];
|
|
495
498
|
|
|
496
499
|
act(() => {
|
|
497
500
|
actions.increment();
|
|
@@ -517,7 +520,9 @@ describe('Selector hooks', () => {
|
|
|
517
520
|
const renderSpy = jest.fn();
|
|
518
521
|
|
|
519
522
|
act(() => {
|
|
520
|
-
root.render(
|
|
523
|
+
root.render(
|
|
524
|
+
<FactoryShortcutConsumer createStateHandler={createStateHandler} onRender={renderSpy} />
|
|
525
|
+
);
|
|
521
526
|
});
|
|
522
527
|
|
|
523
528
|
act(() => {
|
|
@@ -577,7 +582,7 @@ describe('Selector hooks', () => {
|
|
|
577
582
|
return stateHandler;
|
|
578
583
|
});
|
|
579
584
|
const renderSpy = jest.fn();
|
|
580
|
-
const actionsReadySpy = jest.fn();
|
|
585
|
+
const actionsReadySpy = jest.fn<void, [TestActions]>();
|
|
581
586
|
|
|
582
587
|
act(() => {
|
|
583
588
|
root.render(
|
|
@@ -594,7 +599,7 @@ describe('Selector hooks', () => {
|
|
|
594
599
|
counter: 0,
|
|
595
600
|
});
|
|
596
601
|
|
|
597
|
-
const actions = actionsReadySpy.mock.calls
|
|
602
|
+
const [[actions]] = actionsReadySpy.mock.calls as [[TestActions]];
|
|
598
603
|
|
|
599
604
|
act(() => {
|
|
600
605
|
actions.increment();
|
|
@@ -611,16 +616,18 @@ describe('Selector hooks', () => {
|
|
|
611
616
|
user: { name: 'Ada' },
|
|
612
617
|
counter: 0,
|
|
613
618
|
});
|
|
614
|
-
const singleton = makeStateSingleton(() => stateHandler
|
|
619
|
+
const singleton = makeStateSingleton(() => stateHandler, {
|
|
620
|
+
destroyOnNoConsumers: true,
|
|
621
|
+
});
|
|
615
622
|
const firstRenderSpy = jest.fn();
|
|
616
623
|
const secondRenderSpy = jest.fn();
|
|
617
624
|
|
|
618
625
|
act(() => {
|
|
619
626
|
root.render(
|
|
620
|
-
|
|
627
|
+
<React.Fragment>
|
|
621
628
|
<SingletonShortcutConsumer singleton={singleton} onRender={firstRenderSpy} />
|
|
622
629
|
<SingletonShortcutConsumer singleton={singleton} onRender={secondRenderSpy} />
|
|
623
|
-
|
|
630
|
+
</React.Fragment>
|
|
624
631
|
);
|
|
625
632
|
});
|
|
626
633
|
|
|
@@ -645,7 +652,7 @@ describe('Selector hooks', () => {
|
|
|
645
652
|
expect(stateHandler.destroy).not.toHaveBeenCalled();
|
|
646
653
|
|
|
647
654
|
act(() => {
|
|
648
|
-
root.render(
|
|
655
|
+
root.render(<React.Fragment />);
|
|
649
656
|
});
|
|
650
657
|
|
|
651
658
|
expect(stateHandler.destroy).toHaveBeenCalledTimes(1);
|
|
@@ -683,7 +690,7 @@ describe('Selector hooks', () => {
|
|
|
683
690
|
});
|
|
684
691
|
const singleton = makeStateSingleton(() => stateHandler);
|
|
685
692
|
const renderSpy = jest.fn();
|
|
686
|
-
const actionsReadySpy = jest.fn();
|
|
693
|
+
const actionsReadySpy = jest.fn<void, [TestActions]>();
|
|
687
694
|
|
|
688
695
|
act(() => {
|
|
689
696
|
root.render(
|
|
@@ -695,7 +702,7 @@ describe('Selector hooks', () => {
|
|
|
695
702
|
);
|
|
696
703
|
});
|
|
697
704
|
|
|
698
|
-
const actions = actionsReadySpy.mock.calls
|
|
705
|
+
const [[actions]] = actionsReadySpy.mock.calls as [[TestActions]];
|
|
699
706
|
|
|
700
707
|
act(() => {
|
|
701
708
|
actions.increment();
|
|
@@ -710,7 +717,7 @@ describe('Selector hooks', () => {
|
|
|
710
717
|
expect(renderSpy).toHaveBeenCalledTimes(2);
|
|
711
718
|
});
|
|
712
719
|
|
|
713
|
-
it('useStateSingleton selector should
|
|
720
|
+
it('useStateSingleton selector should keep the singleton instance alive by default', () => {
|
|
714
721
|
const firstHandler = new TestStateHandler({
|
|
715
722
|
user: { name: 'Ada' },
|
|
716
723
|
counter: 0,
|
|
@@ -723,9 +730,7 @@ describe('Selector hooks', () => {
|
|
|
723
730
|
.fn(() => firstHandler as StateSubscriptionHandler<TestState, TestActions>)
|
|
724
731
|
.mockReturnValueOnce(firstHandler)
|
|
725
732
|
.mockReturnValueOnce(secondHandler);
|
|
726
|
-
const singleton = makeStateSingleton<TestState, TestActions>(createStateHandler
|
|
727
|
-
destroyOnNoConsumers: false,
|
|
728
|
-
});
|
|
733
|
+
const singleton = makeStateSingleton<TestState, TestActions>(createStateHandler);
|
|
729
734
|
const renderSpy = jest.fn();
|
|
730
735
|
|
|
731
736
|
act(() => {
|
|
@@ -733,7 +738,7 @@ describe('Selector hooks', () => {
|
|
|
733
738
|
});
|
|
734
739
|
|
|
735
740
|
act(() => {
|
|
736
|
-
root.render(
|
|
741
|
+
root.render(<React.Fragment />);
|
|
737
742
|
});
|
|
738
743
|
|
|
739
744
|
expect(firstHandler.destroy).not.toHaveBeenCalled();
|
|
@@ -764,7 +769,10 @@ describe('Selector hooks', () => {
|
|
|
764
769
|
act(() => {
|
|
765
770
|
root.render(
|
|
766
771
|
<React.StrictMode>
|
|
767
|
-
<StrictModeMirrorFactoryConsumer
|
|
772
|
+
<StrictModeMirrorFactoryConsumer
|
|
773
|
+
createStateHandler={createStateHandler}
|
|
774
|
+
onRender={renderSpy}
|
|
775
|
+
/>
|
|
768
776
|
</React.StrictMode>
|
|
769
777
|
);
|
|
770
778
|
});
|
|
@@ -1,16 +1,14 @@
|
|
|
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
|
|
|
4
|
+
import { makeStateSingleton } from '../../../store';
|
|
5
5
|
import { useStateSingleton } from '../state-singleton.js';
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
import type { StateSubscriptionHandler } from '../../types/types.js';
|
|
7
|
+
import type { StateSubscriptionHandler } from '../../../types/types.js';
|
|
10
8
|
|
|
11
9
|
declare global {
|
|
12
10
|
// React 19 requires this flag in test environments that use manual act() calls.
|
|
13
|
-
|
|
11
|
+
|
|
14
12
|
var IS_REACT_ACT_ENVIRONMENT: boolean;
|
|
15
13
|
}
|
|
16
14
|
|
|
@@ -39,7 +37,11 @@ const createStateHandler = (value: number): TestHandler => {
|
|
|
39
37
|
};
|
|
40
38
|
};
|
|
41
39
|
|
|
42
|
-
const SingletonConsumer = ({
|
|
40
|
+
const SingletonConsumer = ({
|
|
41
|
+
singleton,
|
|
42
|
+
}: {
|
|
43
|
+
singleton: ReturnType<typeof makeStateSingleton<TestState, TestActions>>;
|
|
44
|
+
}) => {
|
|
43
45
|
useStateSingleton(singleton);
|
|
44
46
|
|
|
45
47
|
return null;
|
|
@@ -77,10 +79,10 @@ describe('useStateSingleton', () => {
|
|
|
77
79
|
|
|
78
80
|
act(() => {
|
|
79
81
|
root.render(
|
|
80
|
-
|
|
82
|
+
<React.Fragment>
|
|
81
83
|
<SingletonConsumer singleton={singleton} />
|
|
82
84
|
<SingletonConsumer singleton={singleton} />
|
|
83
|
-
|
|
85
|
+
</React.Fragment>
|
|
84
86
|
);
|
|
85
87
|
});
|
|
86
88
|
|
|
@@ -91,27 +93,28 @@ describe('useStateSingleton', () => {
|
|
|
91
93
|
expect(firstHandler.destroy).not.toHaveBeenCalled();
|
|
92
94
|
|
|
93
95
|
act(() => {
|
|
94
|
-
root.render(
|
|
96
|
+
root.render(<React.Fragment />);
|
|
95
97
|
});
|
|
96
98
|
|
|
97
|
-
expect(firstHandler.destroy).
|
|
99
|
+
expect(firstHandler.destroy).not.toHaveBeenCalled();
|
|
98
100
|
});
|
|
99
101
|
|
|
100
|
-
it('should create a new singleton instance after all consumers unmount', () => {
|
|
102
|
+
it('should create a new singleton instance after all consumers unmount when destroyOnNoConsumers is true', () => {
|
|
101
103
|
const firstHandler = createStateHandler(1);
|
|
102
104
|
const secondHandler = createStateHandler(2);
|
|
103
105
|
const factory = jest.fn(() => firstHandler as StateSubscriptionHandler<TestState, TestActions>);
|
|
104
106
|
|
|
105
107
|
factory.mockReturnValueOnce(firstHandler).mockReturnValueOnce(secondHandler);
|
|
106
|
-
|
|
107
|
-
|
|
108
|
+
const singleton = makeStateSingleton<TestState, TestActions>(factory, {
|
|
109
|
+
destroyOnNoConsumers: true,
|
|
110
|
+
});
|
|
108
111
|
|
|
109
112
|
act(() => {
|
|
110
113
|
root.render(<SingletonConsumer singleton={singleton} />);
|
|
111
114
|
});
|
|
112
115
|
|
|
113
116
|
act(() => {
|
|
114
|
-
root.render(
|
|
117
|
+
root.render(<React.Fragment />);
|
|
115
118
|
});
|
|
116
119
|
|
|
117
120
|
expect(firstHandler.destroy).toHaveBeenCalledTimes(1);
|
|
@@ -124,19 +127,17 @@ describe('useStateSingleton', () => {
|
|
|
124
127
|
expect(singleton.getInstance().getSnapshot()).toStrictEqual({ value: 2 });
|
|
125
128
|
});
|
|
126
129
|
|
|
127
|
-
it('should keep singleton instance alive
|
|
130
|
+
it('should keep singleton instance alive by default', () => {
|
|
128
131
|
const firstHandler = createStateHandler(1);
|
|
129
132
|
const factory = jest.fn(() => firstHandler as StateSubscriptionHandler<TestState, TestActions>);
|
|
130
|
-
const singleton = makeStateSingleton<TestState, TestActions>(factory
|
|
131
|
-
destroyOnNoConsumers: false,
|
|
132
|
-
});
|
|
133
|
+
const singleton = makeStateSingleton<TestState, TestActions>(factory);
|
|
133
134
|
|
|
134
135
|
act(() => {
|
|
135
136
|
root.render(<SingletonConsumer singleton={singleton} />);
|
|
136
137
|
});
|
|
137
138
|
|
|
138
139
|
act(() => {
|
|
139
|
-
root.render(
|
|
140
|
+
root.render(<React.Fragment />);
|
|
140
141
|
});
|
|
141
142
|
|
|
142
143
|
expect(firstHandler.destroy).not.toHaveBeenCalled();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { useStateActions } from './state-actions.js';
|
|
2
|
+
export { useStateFactory } from './state-factory.js';
|
|
3
|
+
export { useStateHandler } from './state-handler.js';
|
|
4
|
+
export {
|
|
5
|
+
StateProvider,
|
|
6
|
+
useProvidedStateActions,
|
|
7
|
+
useProvidedStateHandler,
|
|
8
|
+
useProvidedStateSubscription,
|
|
9
|
+
} from './state-provider.js';
|
|
10
|
+
export { useStateSingleton } from './state-singleton.js';
|
|
11
|
+
export { useStateSubscription } from './state-subscription.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
2
|
|
|
3
|
-
import type { StateSubscriptionHandler } from '
|
|
3
|
+
import type { StateSubscriptionHandler } from '../../types/types.js';
|
|
4
4
|
|
|
5
5
|
export function useStateActions<V, A>(stateSubscriptionHandler: StateSubscriptionHandler<V, A>) {
|
|
6
6
|
return useMemo(() => stateSubscriptionHandler.getActions(), [stateSubscriptionHandler]);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useStateHandler } from './state-handler.js';
|
|
2
2
|
import { useStateSubscription } from './state-subscription.js';
|
|
3
3
|
|
|
4
|
-
import type { StateSubscriptionHandler } from '
|
|
4
|
+
import type { StateSubscriptionHandler } from '../../types/types.js';
|
|
5
5
|
|
|
6
6
|
type StateSelector<State, SelectedState> = (state: State) => SelectedState;
|
|
7
7
|
type EqualityFn<SelectedState> = (current: SelectedState, next: SelectedState) => boolean;
|
|
@@ -32,7 +32,7 @@ export function useStateFactory<V, A, P extends unknown[], Sel = V>(
|
|
|
32
32
|
const hasSelector = typeof selectorOrParams === 'function';
|
|
33
33
|
const selector = (hasSelector ? selectorOrParams : identitySelector) as StateSelector<V, Sel>;
|
|
34
34
|
const hasCustomEquality = hasSelector && typeof isEqualOrParams === 'function';
|
|
35
|
-
const isEqual = (hasCustomEquality ? isEqualOrParams : Object.is)
|
|
35
|
+
const isEqual = (hasCustomEquality ? isEqualOrParams : Object.is);
|
|
36
36
|
const stateFactoryParams = (
|
|
37
37
|
hasSelector ? (hasCustomEquality ? params : isEqualOrParams) : selectorOrParams
|
|
38
38
|
) as P;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useRef } from 'react';
|
|
2
2
|
|
|
3
|
-
import type { StateSubscriptionHandler } from '
|
|
3
|
+
import type { StateSubscriptionHandler } from '../../types/types.js';
|
|
4
4
|
|
|
5
5
|
export function useStateHandler<V, A, P extends unknown[]>(
|
|
6
6
|
stateFactoryFunction: (...args: P) => StateSubscriptionHandler<V, A>,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React, { createContext, useContext } from 'react';
|
|
2
|
+
|
|
3
|
+
import { useStateActions } from './state-actions.js';
|
|
4
|
+
import { useStateSubscription } from './state-subscription.js';
|
|
5
|
+
|
|
6
|
+
import type { PropsWithChildren } from 'react';
|
|
7
|
+
import type { StateSubscriptionHandler } from '../../types/types.js';
|
|
8
|
+
|
|
9
|
+
type StateSelector<State, SelectedState> = (state: State) => SelectedState;
|
|
10
|
+
type EqualityFn<SelectedState> = (current: SelectedState, next: SelectedState) => boolean;
|
|
11
|
+
type SharedStateHandler = StateSubscriptionHandler<unknown, unknown>;
|
|
12
|
+
|
|
13
|
+
const StateProviderContext = createContext<SharedStateHandler | null>(null);
|
|
14
|
+
const identitySelector = <State,>(state: State) => state;
|
|
15
|
+
|
|
16
|
+
export type StateProviderProps<V, A> = PropsWithChildren<{
|
|
17
|
+
instance: StateSubscriptionHandler<V, A>;
|
|
18
|
+
}>;
|
|
19
|
+
|
|
20
|
+
export function useProvidedStateHandler<V, A>() {
|
|
21
|
+
const stateHandler = useContext(StateProviderContext);
|
|
22
|
+
|
|
23
|
+
if (!stateHandler) {
|
|
24
|
+
throw new Error('No StateProvider instance found in the current React tree.');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return stateHandler as StateSubscriptionHandler<V, A>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function StateProvider<V, A>({ children, instance }: StateProviderProps<V, A>) {
|
|
31
|
+
return (
|
|
32
|
+
<StateProviderContext.Provider value={instance as SharedStateHandler}>
|
|
33
|
+
{children}
|
|
34
|
+
</StateProviderContext.Provider>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function useProvidedStateActions<V, A>() {
|
|
39
|
+
const stateHandler = useProvidedStateHandler<V, A>();
|
|
40
|
+
|
|
41
|
+
return useStateActions(stateHandler);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function useProvidedStateSubscription<V, A>(): [V, A];
|
|
45
|
+
export function useProvidedStateSubscription<V, A, Sel>(
|
|
46
|
+
selector: StateSelector<V, Sel>,
|
|
47
|
+
isEqual?: EqualityFn<Sel>
|
|
48
|
+
): [Sel, A];
|
|
49
|
+
export function useProvidedStateSubscription<V, A, Sel = V>(
|
|
50
|
+
selector: StateSelector<V, Sel> = identitySelector as StateSelector<V, Sel>,
|
|
51
|
+
isEqual: EqualityFn<Sel> = Object.is
|
|
52
|
+
) {
|
|
53
|
+
const stateHandler = useProvidedStateHandler<V, A>();
|
|
54
|
+
|
|
55
|
+
return useStateSubscription(stateHandler, selector, isEqual);
|
|
56
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useStateSubscription } from './state-subscription.js';
|
|
2
2
|
|
|
3
|
-
import type { StateSingleton } from '
|
|
3
|
+
import type { StateSingleton } from '../../store/state-singleton.js';
|
|
4
4
|
|
|
5
5
|
type StateSelector<State, SelectedState> = (state: State) => SelectedState;
|
|
6
6
|
type EqualityFn<SelectedState> = (current: SelectedState, next: SelectedState) => boolean;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { useCallback,
|
|
2
|
-
import { createSelectorCache, selectWithCache } from '../utils/selector-cache.js';
|
|
1
|
+
import { useCallback, useRef, useSyncExternalStore } from 'react';
|
|
3
2
|
|
|
4
|
-
import
|
|
5
|
-
|
|
3
|
+
import { createSelectorCache, selectWithCache } from '../../utils/selector-cache.js';
|
|
4
|
+
|
|
5
|
+
import type { StateSubscriptionHandler } from '../../types/types.js';
|
|
6
|
+
import type { EqualityFn, Selector } from '../../utils/selector-cache.js';
|
|
6
7
|
|
|
7
8
|
type Listener = () => void;
|
|
8
9
|
type SharedStateSubscriptionHandler = StateSubscriptionHandler<unknown, unknown>;
|
|
@@ -38,7 +39,13 @@ export function useStateSubscriptionSelector<V, A, Sel>(
|
|
|
38
39
|
isEqual: EqualityFn<Sel> = Object.is,
|
|
39
40
|
destroyOnCleanup = true
|
|
40
41
|
) {
|
|
41
|
-
const
|
|
42
|
+
const selectorCacheRef = useRef<ReturnType<typeof createSelectorCache<Sel>> | null>(null);
|
|
43
|
+
|
|
44
|
+
if (!selectorCacheRef.current) {
|
|
45
|
+
selectorCacheRef.current = createSelectorCache<Sel>();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const selectorCache = selectorCacheRef.current;
|
|
42
49
|
|
|
43
50
|
const subscribe = useCallback(
|
|
44
51
|
(listener: Listener) => {
|
|
@@ -75,7 +82,9 @@ export function useStateSubscriptionSelector<V, A, Sel>(
|
|
|
75
82
|
|
|
76
83
|
activeDeferredDestroyState.refCount = 0;
|
|
77
84
|
activeDeferredDestroyState.timeoutId = setTimeout(() => {
|
|
78
|
-
const pendingDeferredDestroyState = deferredDestroyMap.get(
|
|
85
|
+
const pendingDeferredDestroyState = deferredDestroyMap.get(
|
|
86
|
+
sharedStateSubscriptionHandler
|
|
87
|
+
);
|
|
79
88
|
|
|
80
89
|
if (!pendingDeferredDestroyState || pendingDeferredDestroyState.refCount > 0) {
|
|
81
90
|
return;
|
|
@@ -3,8 +3,8 @@ import { useEffect, useMemo } from 'react';
|
|
|
3
3
|
import { useStateActions } from './state-actions.js';
|
|
4
4
|
import { useStateSubscriptionSelector } from './state-subscription-selector.js';
|
|
5
5
|
|
|
6
|
-
import type { StateSingleton } from '
|
|
7
|
-
import type { StateSubscriptionHandler } from '
|
|
6
|
+
import type { StateSingleton } from '../../store/state-singleton.js';
|
|
7
|
+
import type { StateSubscriptionHandler } from '../../types/types.js';
|
|
8
8
|
|
|
9
9
|
type StateSelector<State, SelectedState> = (state: State) => SelectedState;
|
|
10
10
|
type EqualityFn<SelectedState> = (current: SelectedState, next: SelectedState) => boolean;
|
|
@@ -27,17 +27,13 @@ function isStateSingleton<V, A>(
|
|
|
27
27
|
return 'getInstance' in source;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
export function useStateSubscription<V, A>(
|
|
31
|
-
source: StateSubscriptionHandler<V, A>
|
|
32
|
-
): [V, A];
|
|
30
|
+
export function useStateSubscription<V, A>(source: StateSubscriptionHandler<V, A>): [V, A];
|
|
33
31
|
export function useStateSubscription<V, A, Sel>(
|
|
34
32
|
source: StateSubscriptionHandler<V, A>,
|
|
35
33
|
selector: StateSelector<V, Sel>,
|
|
36
34
|
isEqual?: EqualityFn<Sel>
|
|
37
35
|
): [Sel, A];
|
|
38
|
-
export function useStateSubscription<V, A>(
|
|
39
|
-
source: StateSingleton<V, A>
|
|
40
|
-
): [V, A];
|
|
36
|
+
export function useStateSubscription<V, A>(source: StateSingleton<V, A>): [V, A];
|
|
41
37
|
export function useStateSubscription<V, A, Sel>(
|
|
42
38
|
source: StateSingleton<V, A>,
|
|
43
39
|
selector: StateSelector<V, Sel>,
|
|
@@ -83,7 +79,7 @@ export function useStateSubscription<V, A, Sel = V>(
|
|
|
83
79
|
|
|
84
80
|
if (activeReference.count <= 0) {
|
|
85
81
|
singletonReferences.delete(singleton);
|
|
86
|
-
if (singleton.destroyOnNoConsumers
|
|
82
|
+
if (singleton.destroyOnNoConsumers !== true) {
|
|
87
83
|
return;
|
|
88
84
|
}
|
|
89
85
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './hooks/index.js';
|