ccstate-react 4.12.0 → 4.13.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/CHANGELOG.md +10 -0
- package/dist/index.cjs +48 -29
- package/dist/index.js +49 -30
- package/package.json +3 -3
- package/src/__tests__/loadable.test.tsx +44 -4
- package/src/useGet.ts +3 -22
- package/src/useLoadable.ts +53 -12
package/CHANGELOG.md
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -13,30 +13,12 @@ function useStore() {
|
|
|
13
13
|
return store;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
function
|
|
17
|
-
var silenceUnhandleRejection = _ref.silenceUnhandleRejection;
|
|
16
|
+
function useGet(atom) {
|
|
18
17
|
var store = useStore();
|
|
19
18
|
return react.useSyncExternalStore(function (fn) {
|
|
20
|
-
|
|
21
|
-
store.sub(atom, ccstate.command(fn), {
|
|
22
|
-
signal: ctrl.signal
|
|
23
|
-
});
|
|
24
|
-
return function () {
|
|
25
|
-
ctrl.abort();
|
|
26
|
-
};
|
|
19
|
+
return store.sub(atom, ccstate.command(fn));
|
|
27
20
|
}, function () {
|
|
28
|
-
|
|
29
|
-
if (val instanceof Promise && silenceUnhandleRejection) {
|
|
30
|
-
val["catch"](function () {
|
|
31
|
-
return void 0;
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
return val;
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
function useGet(atom) {
|
|
38
|
-
return useGetInternal(atom, {
|
|
39
|
-
silenceUnhandleRejection: false
|
|
21
|
+
return store.get(atom);
|
|
40
22
|
});
|
|
41
23
|
}
|
|
42
24
|
|
|
@@ -98,10 +80,47 @@ function _unsupportedIterableToArray(r, a) {
|
|
|
98
80
|
}
|
|
99
81
|
}
|
|
100
82
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Handles a specific behavior of useSyncExternalStore. In React, there are situations where the getSnapshot function of
|
|
85
|
+
* useSyncExternalStore executes, but the Render function doesn't execute.
|
|
86
|
+
*
|
|
87
|
+
* This can cause the promise generated in that round to not be caught, and userspace has no opportunity to handle this
|
|
88
|
+
* promise. Therefore, this issue needs to be handled in useGetPromise.
|
|
89
|
+
*
|
|
90
|
+
* @param atom
|
|
91
|
+
* @returns
|
|
92
|
+
*/
|
|
93
|
+
function useGetPromise(atom) {
|
|
94
|
+
var store = useStore();
|
|
95
|
+
var lastPromise = react.useRef(undefined);
|
|
96
|
+
var promiseProcessed = react.useRef(false);
|
|
97
|
+
var promise = react.useSyncExternalStore(function (fn) {
|
|
98
|
+
return store.sub(atom, ccstate.command(fn));
|
|
99
|
+
}, function () {
|
|
100
|
+
var val = store.get(atom);
|
|
101
|
+
|
|
102
|
+
// If the last promise is not processed and the current value is a promise,
|
|
103
|
+
// we need to silence the last promise to avoid unhandled rejections.
|
|
104
|
+
if (lastPromise.current !== undefined && lastPromise.current !== val && !promiseProcessed.current) {
|
|
105
|
+
lastPromise.current["catch"](function () {
|
|
106
|
+
return void 0;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (lastPromise.current !== val) {
|
|
110
|
+
promiseProcessed.current = false;
|
|
111
|
+
lastPromise.current = val instanceof Promise ? val : undefined;
|
|
112
|
+
}
|
|
113
|
+
return val;
|
|
104
114
|
});
|
|
115
|
+
return [promise, function () {
|
|
116
|
+
promiseProcessed.current = true;
|
|
117
|
+
}];
|
|
118
|
+
}
|
|
119
|
+
function useLoadableInternal(atom, keepLastResolved) {
|
|
120
|
+
var _useGetPromise = useGetPromise(atom),
|
|
121
|
+
_useGetPromise2 = _slicedToArray(_useGetPromise, 2),
|
|
122
|
+
promise = _useGetPromise2[0],
|
|
123
|
+
setPromiseProcessed = _useGetPromise2[1];
|
|
105
124
|
var _useState = react.useState({
|
|
106
125
|
state: 'loading'
|
|
107
126
|
}),
|
|
@@ -116,28 +135,28 @@ function useLoadableInternal(atom, keepLastResolved) {
|
|
|
116
135
|
});
|
|
117
136
|
return;
|
|
118
137
|
}
|
|
119
|
-
var
|
|
120
|
-
var signal = ctrl.signal;
|
|
138
|
+
var cancelled = false;
|
|
121
139
|
if (!keepLastResolved) {
|
|
122
140
|
setPromiseResult({
|
|
123
141
|
state: 'loading'
|
|
124
142
|
});
|
|
125
143
|
}
|
|
144
|
+
setPromiseProcessed();
|
|
126
145
|
promise.then(function (ret) {
|
|
127
|
-
if (
|
|
146
|
+
if (cancelled) return;
|
|
128
147
|
setPromiseResult({
|
|
129
148
|
state: 'hasData',
|
|
130
149
|
data: ret
|
|
131
150
|
});
|
|
132
151
|
}, function (error) {
|
|
133
|
-
if (
|
|
152
|
+
if (cancelled) return;
|
|
134
153
|
setPromiseResult({
|
|
135
154
|
state: 'hasError',
|
|
136
155
|
error: error
|
|
137
156
|
});
|
|
138
157
|
});
|
|
139
158
|
return function () {
|
|
140
|
-
|
|
159
|
+
cancelled = true;
|
|
141
160
|
};
|
|
142
161
|
}, [promise]);
|
|
143
162
|
return promiseResult;
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createContext, useContext, useSyncExternalStore, useCallback, useState, useEffect } from 'react';
|
|
1
|
+
import { createContext, useContext, useSyncExternalStore, useCallback, useState, useEffect, useRef } from 'react';
|
|
2
2
|
import { getDefaultStore, command } from 'ccstate';
|
|
3
3
|
|
|
4
4
|
var StoreContext = createContext(null);
|
|
@@ -11,30 +11,12 @@ function useStore() {
|
|
|
11
11
|
return store;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
function
|
|
15
|
-
var silenceUnhandleRejection = _ref.silenceUnhandleRejection;
|
|
14
|
+
function useGet(atom) {
|
|
16
15
|
var store = useStore();
|
|
17
16
|
return useSyncExternalStore(function (fn) {
|
|
18
|
-
|
|
19
|
-
store.sub(atom, command(fn), {
|
|
20
|
-
signal: ctrl.signal
|
|
21
|
-
});
|
|
22
|
-
return function () {
|
|
23
|
-
ctrl.abort();
|
|
24
|
-
};
|
|
17
|
+
return store.sub(atom, command(fn));
|
|
25
18
|
}, function () {
|
|
26
|
-
|
|
27
|
-
if (val instanceof Promise && silenceUnhandleRejection) {
|
|
28
|
-
val["catch"](function () {
|
|
29
|
-
return void 0;
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
return val;
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
function useGet(atom) {
|
|
36
|
-
return useGetInternal(atom, {
|
|
37
|
-
silenceUnhandleRejection: false
|
|
19
|
+
return store.get(atom);
|
|
38
20
|
});
|
|
39
21
|
}
|
|
40
22
|
|
|
@@ -96,10 +78,47 @@ function _unsupportedIterableToArray(r, a) {
|
|
|
96
78
|
}
|
|
97
79
|
}
|
|
98
80
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Handles a specific behavior of useSyncExternalStore. In React, there are situations where the getSnapshot function of
|
|
83
|
+
* useSyncExternalStore executes, but the Render function doesn't execute.
|
|
84
|
+
*
|
|
85
|
+
* This can cause the promise generated in that round to not be caught, and userspace has no opportunity to handle this
|
|
86
|
+
* promise. Therefore, this issue needs to be handled in useGetPromise.
|
|
87
|
+
*
|
|
88
|
+
* @param atom
|
|
89
|
+
* @returns
|
|
90
|
+
*/
|
|
91
|
+
function useGetPromise(atom) {
|
|
92
|
+
var store = useStore();
|
|
93
|
+
var lastPromise = useRef(undefined);
|
|
94
|
+
var promiseProcessed = useRef(false);
|
|
95
|
+
var promise = useSyncExternalStore(function (fn) {
|
|
96
|
+
return store.sub(atom, command(fn));
|
|
97
|
+
}, function () {
|
|
98
|
+
var val = store.get(atom);
|
|
99
|
+
|
|
100
|
+
// If the last promise is not processed and the current value is a promise,
|
|
101
|
+
// we need to silence the last promise to avoid unhandled rejections.
|
|
102
|
+
if (lastPromise.current !== undefined && lastPromise.current !== val && !promiseProcessed.current) {
|
|
103
|
+
lastPromise.current["catch"](function () {
|
|
104
|
+
return void 0;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
if (lastPromise.current !== val) {
|
|
108
|
+
promiseProcessed.current = false;
|
|
109
|
+
lastPromise.current = val instanceof Promise ? val : undefined;
|
|
110
|
+
}
|
|
111
|
+
return val;
|
|
102
112
|
});
|
|
113
|
+
return [promise, function () {
|
|
114
|
+
promiseProcessed.current = true;
|
|
115
|
+
}];
|
|
116
|
+
}
|
|
117
|
+
function useLoadableInternal(atom, keepLastResolved) {
|
|
118
|
+
var _useGetPromise = useGetPromise(atom),
|
|
119
|
+
_useGetPromise2 = _slicedToArray(_useGetPromise, 2),
|
|
120
|
+
promise = _useGetPromise2[0],
|
|
121
|
+
setPromiseProcessed = _useGetPromise2[1];
|
|
103
122
|
var _useState = useState({
|
|
104
123
|
state: 'loading'
|
|
105
124
|
}),
|
|
@@ -114,28 +133,28 @@ function useLoadableInternal(atom, keepLastResolved) {
|
|
|
114
133
|
});
|
|
115
134
|
return;
|
|
116
135
|
}
|
|
117
|
-
var
|
|
118
|
-
var signal = ctrl.signal;
|
|
136
|
+
var cancelled = false;
|
|
119
137
|
if (!keepLastResolved) {
|
|
120
138
|
setPromiseResult({
|
|
121
139
|
state: 'loading'
|
|
122
140
|
});
|
|
123
141
|
}
|
|
142
|
+
setPromiseProcessed();
|
|
124
143
|
promise.then(function (ret) {
|
|
125
|
-
if (
|
|
144
|
+
if (cancelled) return;
|
|
126
145
|
setPromiseResult({
|
|
127
146
|
state: 'hasData',
|
|
128
147
|
data: ret
|
|
129
148
|
});
|
|
130
149
|
}, function (error) {
|
|
131
|
-
if (
|
|
150
|
+
if (cancelled) return;
|
|
132
151
|
setPromiseResult({
|
|
133
152
|
state: 'hasError',
|
|
134
153
|
error: error
|
|
135
154
|
});
|
|
136
155
|
});
|
|
137
156
|
return function () {
|
|
138
|
-
|
|
157
|
+
cancelled = true;
|
|
139
158
|
};
|
|
140
159
|
}, [promise]);
|
|
141
160
|
return promiseResult;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccstate-react",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.13.0",
|
|
4
4
|
"description": "CCState React Hooks",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"react": ">=17.0.0"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"ccstate": "^4.
|
|
28
|
+
"ccstate": "^4.13.0"
|
|
29
29
|
},
|
|
30
30
|
"peerDependenciesMeta": {
|
|
31
31
|
"@types/react": {
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"shx": "^0.3.4",
|
|
56
56
|
"signal-timers": "^1.0.4",
|
|
57
57
|
"vitest": "^2.1.8",
|
|
58
|
-
"ccstate": "^4.
|
|
58
|
+
"ccstate": "^4.13.0"
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
61
|
"build": "rollup -c",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import '@testing-library/jest-dom/vitest';
|
|
4
4
|
import { render, cleanup, screen } from '@testing-library/react';
|
|
5
5
|
import userEvent from '@testing-library/user-event';
|
|
6
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, test, vi } from 'vitest';
|
|
7
7
|
import { computed, createStore, state } from 'ccstate';
|
|
8
8
|
import type { Computed, State } from 'ccstate';
|
|
9
9
|
import { StrictMode, useEffect } from 'react';
|
|
@@ -583,9 +583,11 @@ describe('works with AbortError', () => {
|
|
|
583
583
|
}
|
|
584
584
|
|
|
585
585
|
render(
|
|
586
|
-
<
|
|
587
|
-
<
|
|
588
|
-
|
|
586
|
+
<StrictMode>
|
|
587
|
+
<StoreProvider value={store}>
|
|
588
|
+
<App />
|
|
589
|
+
</StoreProvider>
|
|
590
|
+
</StrictMode>,
|
|
589
591
|
);
|
|
590
592
|
|
|
591
593
|
expect(screen.getByText('Test')).toBeInTheDocument();
|
|
@@ -594,3 +596,41 @@ describe('works with AbortError', () => {
|
|
|
594
596
|
store.set(reload$, (x) => x + 1);
|
|
595
597
|
});
|
|
596
598
|
});
|
|
599
|
+
|
|
600
|
+
test('useLoadable should catch errors', () => {
|
|
601
|
+
const reload$ = state(0);
|
|
602
|
+
|
|
603
|
+
const traceCatch = vi.fn();
|
|
604
|
+
const promise$ = computed((get) => {
|
|
605
|
+
get(reload$);
|
|
606
|
+
|
|
607
|
+
const p = Promise.resolve();
|
|
608
|
+
const originalCatch = p.catch.bind(p);
|
|
609
|
+
vi.spyOn(p, 'catch').mockImplementation((...args) => {
|
|
610
|
+
traceCatch();
|
|
611
|
+
return originalCatch(...args);
|
|
612
|
+
});
|
|
613
|
+
return p;
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
const store = createStore();
|
|
617
|
+
|
|
618
|
+
function App() {
|
|
619
|
+
useLoadable(promise$);
|
|
620
|
+
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
render(
|
|
625
|
+
<StrictMode>
|
|
626
|
+
<StoreProvider value={store}>
|
|
627
|
+
<App />
|
|
628
|
+
</StoreProvider>
|
|
629
|
+
</StrictMode>,
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
store.set(reload$, (x) => x + 1);
|
|
633
|
+
store.set(reload$, (x) => x + 1);
|
|
634
|
+
|
|
635
|
+
expect(traceCatch).toHaveBeenCalledTimes(1);
|
|
636
|
+
});
|
package/src/useGet.ts
CHANGED
|
@@ -3,29 +3,10 @@ import { useStore } from './provider';
|
|
|
3
3
|
import { command } from 'ccstate';
|
|
4
4
|
import type { Computed, State } from 'ccstate';
|
|
5
5
|
|
|
6
|
-
export function
|
|
7
|
-
atom: State<T> | Computed<T>,
|
|
8
|
-
{ silenceUnhandleRejection }: { silenceUnhandleRejection: boolean },
|
|
9
|
-
) {
|
|
6
|
+
export function useGet<T>(atom: State<T> | Computed<T>) {
|
|
10
7
|
const store = useStore();
|
|
11
8
|
return useSyncExternalStore(
|
|
12
|
-
(fn) =>
|
|
13
|
-
|
|
14
|
-
store.sub(atom, command(fn), { signal: ctrl.signal });
|
|
15
|
-
return () => {
|
|
16
|
-
ctrl.abort();
|
|
17
|
-
};
|
|
18
|
-
},
|
|
19
|
-
() => {
|
|
20
|
-
const val = store.get(atom);
|
|
21
|
-
if (val instanceof Promise && silenceUnhandleRejection) {
|
|
22
|
-
val.catch(() => void 0);
|
|
23
|
-
}
|
|
24
|
-
return val;
|
|
25
|
-
},
|
|
9
|
+
(fn) => store.sub(atom, command(fn)),
|
|
10
|
+
() => store.get(atom),
|
|
26
11
|
);
|
|
27
12
|
}
|
|
28
|
-
|
|
29
|
-
export function useGet<T>(atom: State<T> | Computed<T>) {
|
|
30
|
-
return useGetInternal(atom, { silenceUnhandleRejection: false });
|
|
31
|
-
}
|
package/src/useLoadable.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
1
|
+
import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
2
|
+
import { command, type Computed, type State } from 'ccstate';
|
|
3
|
+
import { useStore } from './provider';
|
|
4
4
|
|
|
5
5
|
type Loadable<T> =
|
|
6
6
|
| {
|
|
@@ -15,14 +15,54 @@ type Loadable<T> =
|
|
|
15
15
|
error: unknown;
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Handles a specific behavior of useSyncExternalStore. In React, there are situations where the getSnapshot function of
|
|
20
|
+
* useSyncExternalStore executes, but the Render function doesn't execute.
|
|
21
|
+
*
|
|
22
|
+
* This can cause the promise generated in that round to not be caught, and userspace has no opportunity to handle this
|
|
23
|
+
* promise. Therefore, this issue needs to be handled in useGetPromise.
|
|
24
|
+
*
|
|
25
|
+
* @param atom
|
|
26
|
+
* @returns
|
|
27
|
+
*/
|
|
28
|
+
function useGetPromise<T, U extends Promise<T> | T>(atom: State<U> | Computed<U>): [U, () => void] {
|
|
29
|
+
const store = useStore();
|
|
30
|
+
const lastPromise = useRef<Promise<unknown> | undefined>(undefined);
|
|
31
|
+
const promiseProcessed = useRef(false);
|
|
32
|
+
|
|
33
|
+
const promise = useSyncExternalStore(
|
|
34
|
+
(fn) => store.sub(atom, command(fn)),
|
|
35
|
+
() => {
|
|
36
|
+
const val = store.get(atom);
|
|
37
|
+
|
|
38
|
+
// If the last promise is not processed and the current value is a promise,
|
|
39
|
+
// we need to silence the last promise to avoid unhandled rejections.
|
|
40
|
+
if (lastPromise.current !== undefined && lastPromise.current !== val && !promiseProcessed.current) {
|
|
41
|
+
lastPromise.current.catch(() => void 0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (lastPromise.current !== val) {
|
|
45
|
+
promiseProcessed.current = false;
|
|
46
|
+
lastPromise.current = val instanceof Promise ? val : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return val;
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return [
|
|
54
|
+
promise,
|
|
55
|
+
() => {
|
|
56
|
+
promiseProcessed.current = true;
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
|
|
18
61
|
function useLoadableInternal<T>(
|
|
19
62
|
atom: State<Promise<T> | T> | Computed<Promise<T> | T>,
|
|
20
63
|
keepLastResolved: boolean,
|
|
21
64
|
): Loadable<T> {
|
|
22
|
-
const promise =
|
|
23
|
-
silenceUnhandleRejection: true,
|
|
24
|
-
});
|
|
25
|
-
|
|
65
|
+
const [promise, setPromiseProcessed] = useGetPromise(atom);
|
|
26
66
|
const [promiseResult, setPromiseResult] = useState<Loadable<T>>({
|
|
27
67
|
state: 'loading',
|
|
28
68
|
});
|
|
@@ -37,8 +77,7 @@ function useLoadableInternal<T>(
|
|
|
37
77
|
return;
|
|
38
78
|
}
|
|
39
79
|
|
|
40
|
-
|
|
41
|
-
const signal = ctrl.signal;
|
|
80
|
+
let cancelled = false;
|
|
42
81
|
|
|
43
82
|
if (!keepLastResolved) {
|
|
44
83
|
setPromiseResult({
|
|
@@ -46,9 +85,11 @@ function useLoadableInternal<T>(
|
|
|
46
85
|
});
|
|
47
86
|
}
|
|
48
87
|
|
|
88
|
+
setPromiseProcessed();
|
|
89
|
+
|
|
49
90
|
promise.then(
|
|
50
91
|
(ret) => {
|
|
51
|
-
if (
|
|
92
|
+
if (cancelled) return;
|
|
52
93
|
|
|
53
94
|
setPromiseResult({
|
|
54
95
|
state: 'hasData',
|
|
@@ -56,7 +97,7 @@ function useLoadableInternal<T>(
|
|
|
56
97
|
});
|
|
57
98
|
},
|
|
58
99
|
(error: unknown) => {
|
|
59
|
-
if (
|
|
100
|
+
if (cancelled) return;
|
|
60
101
|
|
|
61
102
|
setPromiseResult({
|
|
62
103
|
state: 'hasError',
|
|
@@ -66,7 +107,7 @@ function useLoadableInternal<T>(
|
|
|
66
107
|
);
|
|
67
108
|
|
|
68
109
|
return () => {
|
|
69
|
-
|
|
110
|
+
cancelled = true;
|
|
70
111
|
};
|
|
71
112
|
}, [promise]);
|
|
72
113
|
|