ccstate-react 4.11.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 +20 -0
- package/dist/index.cjs +46 -13
- package/dist/index.js +47 -14
- package/package.json +3 -3
- package/src/__tests__/loadable.test.tsx +92 -1
- package/src/useGet.ts +2 -10
- package/src/useLoadable.ts +53 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# ccstate-react
|
|
2
2
|
|
|
3
|
+
## 4.13.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 8f06427: refactor: avoid unhandledRejection in useLoadable
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- ccstate@4.13.0
|
|
12
|
+
|
|
13
|
+
## 4.12.0
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- c8f4fca: fix(react): add global error handler for floating promises in useLoadable
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- ccstate@4.12.0
|
|
22
|
+
|
|
3
23
|
## 4.11.0
|
|
4
24
|
|
|
5
25
|
### Minor Changes
|
package/dist/index.cjs
CHANGED
|
@@ -16,13 +16,7 @@ function useStore() {
|
|
|
16
16
|
function useGet(atom) {
|
|
17
17
|
var store = useStore();
|
|
18
18
|
return react.useSyncExternalStore(function (fn) {
|
|
19
|
-
|
|
20
|
-
store.sub(atom, ccstate.command(fn), {
|
|
21
|
-
signal: ctrl.signal
|
|
22
|
-
});
|
|
23
|
-
return function () {
|
|
24
|
-
ctrl.abort();
|
|
25
|
-
};
|
|
19
|
+
return store.sub(atom, ccstate.command(fn));
|
|
26
20
|
}, function () {
|
|
27
21
|
return store.get(atom);
|
|
28
22
|
});
|
|
@@ -86,8 +80,47 @@ function _unsupportedIterableToArray(r, a) {
|
|
|
86
80
|
}
|
|
87
81
|
}
|
|
88
82
|
|
|
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;
|
|
114
|
+
});
|
|
115
|
+
return [promise, function () {
|
|
116
|
+
promiseProcessed.current = true;
|
|
117
|
+
}];
|
|
118
|
+
}
|
|
89
119
|
function useLoadableInternal(atom, keepLastResolved) {
|
|
90
|
-
var
|
|
120
|
+
var _useGetPromise = useGetPromise(atom),
|
|
121
|
+
_useGetPromise2 = _slicedToArray(_useGetPromise, 2),
|
|
122
|
+
promise = _useGetPromise2[0],
|
|
123
|
+
setPromiseProcessed = _useGetPromise2[1];
|
|
91
124
|
var _useState = react.useState({
|
|
92
125
|
state: 'loading'
|
|
93
126
|
}),
|
|
@@ -102,28 +135,28 @@ function useLoadableInternal(atom, keepLastResolved) {
|
|
|
102
135
|
});
|
|
103
136
|
return;
|
|
104
137
|
}
|
|
105
|
-
var
|
|
106
|
-
var signal = ctrl.signal;
|
|
138
|
+
var cancelled = false;
|
|
107
139
|
if (!keepLastResolved) {
|
|
108
140
|
setPromiseResult({
|
|
109
141
|
state: 'loading'
|
|
110
142
|
});
|
|
111
143
|
}
|
|
144
|
+
setPromiseProcessed();
|
|
112
145
|
promise.then(function (ret) {
|
|
113
|
-
if (
|
|
146
|
+
if (cancelled) return;
|
|
114
147
|
setPromiseResult({
|
|
115
148
|
state: 'hasData',
|
|
116
149
|
data: ret
|
|
117
150
|
});
|
|
118
151
|
}, function (error) {
|
|
119
|
-
if (
|
|
152
|
+
if (cancelled) return;
|
|
120
153
|
setPromiseResult({
|
|
121
154
|
state: 'hasError',
|
|
122
155
|
error: error
|
|
123
156
|
});
|
|
124
157
|
});
|
|
125
158
|
return function () {
|
|
126
|
-
|
|
159
|
+
cancelled = true;
|
|
127
160
|
};
|
|
128
161
|
}, [promise]);
|
|
129
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);
|
|
@@ -14,13 +14,7 @@ function useStore() {
|
|
|
14
14
|
function useGet(atom) {
|
|
15
15
|
var store = useStore();
|
|
16
16
|
return useSyncExternalStore(function (fn) {
|
|
17
|
-
|
|
18
|
-
store.sub(atom, command(fn), {
|
|
19
|
-
signal: ctrl.signal
|
|
20
|
-
});
|
|
21
|
-
return function () {
|
|
22
|
-
ctrl.abort();
|
|
23
|
-
};
|
|
17
|
+
return store.sub(atom, command(fn));
|
|
24
18
|
}, function () {
|
|
25
19
|
return store.get(atom);
|
|
26
20
|
});
|
|
@@ -84,8 +78,47 @@ function _unsupportedIterableToArray(r, a) {
|
|
|
84
78
|
}
|
|
85
79
|
}
|
|
86
80
|
|
|
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;
|
|
112
|
+
});
|
|
113
|
+
return [promise, function () {
|
|
114
|
+
promiseProcessed.current = true;
|
|
115
|
+
}];
|
|
116
|
+
}
|
|
87
117
|
function useLoadableInternal(atom, keepLastResolved) {
|
|
88
|
-
var
|
|
118
|
+
var _useGetPromise = useGetPromise(atom),
|
|
119
|
+
_useGetPromise2 = _slicedToArray(_useGetPromise, 2),
|
|
120
|
+
promise = _useGetPromise2[0],
|
|
121
|
+
setPromiseProcessed = _useGetPromise2[1];
|
|
89
122
|
var _useState = useState({
|
|
90
123
|
state: 'loading'
|
|
91
124
|
}),
|
|
@@ -100,28 +133,28 @@ function useLoadableInternal(atom, keepLastResolved) {
|
|
|
100
133
|
});
|
|
101
134
|
return;
|
|
102
135
|
}
|
|
103
|
-
var
|
|
104
|
-
var signal = ctrl.signal;
|
|
136
|
+
var cancelled = false;
|
|
105
137
|
if (!keepLastResolved) {
|
|
106
138
|
setPromiseResult({
|
|
107
139
|
state: 'loading'
|
|
108
140
|
});
|
|
109
141
|
}
|
|
142
|
+
setPromiseProcessed();
|
|
110
143
|
promise.then(function (ret) {
|
|
111
|
-
if (
|
|
144
|
+
if (cancelled) return;
|
|
112
145
|
setPromiseResult({
|
|
113
146
|
state: 'hasData',
|
|
114
147
|
data: ret
|
|
115
148
|
});
|
|
116
149
|
}, function (error) {
|
|
117
|
-
if (
|
|
150
|
+
if (cancelled) return;
|
|
118
151
|
setPromiseResult({
|
|
119
152
|
state: 'hasError',
|
|
120
153
|
error: error
|
|
121
154
|
});
|
|
122
155
|
});
|
|
123
156
|
return function () {
|
|
124
|
-
|
|
157
|
+
cancelled = true;
|
|
125
158
|
};
|
|
126
159
|
}, [promise]);
|
|
127
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, 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';
|
|
@@ -543,3 +543,94 @@ it('useLoadable accept sync computed', async () => {
|
|
|
543
543
|
|
|
544
544
|
expect(await screen.findByText('hasData')).toBeInTheDocument();
|
|
545
545
|
});
|
|
546
|
+
|
|
547
|
+
describe('works with AbortError', () => {
|
|
548
|
+
let abortController: AbortController;
|
|
549
|
+
let promise: Promise<void>;
|
|
550
|
+
|
|
551
|
+
beforeEach(() => {
|
|
552
|
+
abortController = new AbortController();
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
afterEach(async () => {
|
|
556
|
+
abortController.abort();
|
|
557
|
+
|
|
558
|
+
try {
|
|
559
|
+
await promise;
|
|
560
|
+
} catch {}
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it('should not throw abortError', () => {
|
|
564
|
+
const store = createStore();
|
|
565
|
+
const signal = abortController.signal;
|
|
566
|
+
|
|
567
|
+
promise = new Promise((_resolve, reject) => {
|
|
568
|
+
signal.addEventListener('abort', () => {
|
|
569
|
+
reject(signal.reason as Error);
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
const reload$ = state(0);
|
|
574
|
+
const promise$ = computed(async (get) => {
|
|
575
|
+
get(reload$);
|
|
576
|
+
await promise;
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
function App() {
|
|
580
|
+
useLoadable(promise$);
|
|
581
|
+
|
|
582
|
+
return <div>Test</div>;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
render(
|
|
586
|
+
<StrictMode>
|
|
587
|
+
<StoreProvider value={store}>
|
|
588
|
+
<App />
|
|
589
|
+
</StoreProvider>
|
|
590
|
+
</StrictMode>,
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
expect(screen.getByText('Test')).toBeInTheDocument();
|
|
594
|
+
|
|
595
|
+
store.set(reload$, (x) => x + 1);
|
|
596
|
+
store.set(reload$, (x) => x + 1);
|
|
597
|
+
});
|
|
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
|
@@ -6,15 +6,7 @@ import type { Computed, State } from 'ccstate';
|
|
|
6
6
|
export function useGet<T>(atom: State<T> | Computed<T>) {
|
|
7
7
|
const store = useStore();
|
|
8
8
|
return useSyncExternalStore(
|
|
9
|
-
(fn) =>
|
|
10
|
-
|
|
11
|
-
store.sub(atom, command(fn), { signal: ctrl.signal });
|
|
12
|
-
return () => {
|
|
13
|
-
ctrl.abort();
|
|
14
|
-
};
|
|
15
|
-
},
|
|
16
|
-
() => {
|
|
17
|
-
return store.get(atom);
|
|
18
|
-
},
|
|
9
|
+
(fn) => store.sub(atom, command(fn)),
|
|
10
|
+
() => store.get(atom),
|
|
19
11
|
);
|
|
20
12
|
}
|
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,12 +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
|
-
|
|
65
|
+
const [promise, setPromiseProcessed] = useGetPromise(atom);
|
|
24
66
|
const [promiseResult, setPromiseResult] = useState<Loadable<T>>({
|
|
25
67
|
state: 'loading',
|
|
26
68
|
});
|
|
@@ -35,8 +77,7 @@ function useLoadableInternal<T>(
|
|
|
35
77
|
return;
|
|
36
78
|
}
|
|
37
79
|
|
|
38
|
-
|
|
39
|
-
const signal = ctrl.signal;
|
|
80
|
+
let cancelled = false;
|
|
40
81
|
|
|
41
82
|
if (!keepLastResolved) {
|
|
42
83
|
setPromiseResult({
|
|
@@ -44,9 +85,11 @@ function useLoadableInternal<T>(
|
|
|
44
85
|
});
|
|
45
86
|
}
|
|
46
87
|
|
|
88
|
+
setPromiseProcessed();
|
|
89
|
+
|
|
47
90
|
promise.then(
|
|
48
91
|
(ret) => {
|
|
49
|
-
if (
|
|
92
|
+
if (cancelled) return;
|
|
50
93
|
|
|
51
94
|
setPromiseResult({
|
|
52
95
|
state: 'hasData',
|
|
@@ -54,7 +97,7 @@ function useLoadableInternal<T>(
|
|
|
54
97
|
});
|
|
55
98
|
},
|
|
56
99
|
(error: unknown) => {
|
|
57
|
-
if (
|
|
100
|
+
if (cancelled) return;
|
|
58
101
|
|
|
59
102
|
setPromiseResult({
|
|
60
103
|
state: 'hasError',
|
|
@@ -64,7 +107,7 @@ function useLoadableInternal<T>(
|
|
|
64
107
|
);
|
|
65
108
|
|
|
66
109
|
return () => {
|
|
67
|
-
|
|
110
|
+
cancelled = true;
|
|
68
111
|
};
|
|
69
112
|
}, [promise]);
|
|
70
113
|
|