ccstate-react 4.13.0 → 5.2.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 +38 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/clover.xml +219 -0
- package/coverage/coverage-final.json +8 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +206 -0
- package/coverage/index.ts.html +100 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/provider.ts.html +133 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +196 -0
- package/coverage/useGet.ts.html +160 -0
- package/coverage/useLoadable.ts.html +403 -0
- package/coverage/useLoadableSet.ts.html +295 -0
- package/coverage/useResolved.ts.html +133 -0
- package/coverage/useSet.ts.html +166 -0
- package/dist/experimental.cjs +92 -45
- package/dist/experimental.d.cts +15 -6
- package/dist/experimental.d.ts +15 -6
- package/dist/experimental.js +93 -43
- package/dist/index.cjs +56 -125
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +57 -126
- package/package.json +9 -9
- package/src/__tests__/get-and-set.test.tsx +22 -13
- package/src/__tests__/loadable-set.test.tsx +255 -0
- package/src/__tests__/loadable.test.tsx +14 -6
- package/src/__tests__/resolved.test.tsx +6 -1
- package/src/experimental.ts +1 -1
- package/src/provider.ts +1 -2
- package/src/useGet.ts +19 -6
- package/src/useLoadable.ts +74 -95
- package/src/useLoadableSet.ts +72 -0
- package/src/useResolved.ts +2 -2
- package/LICENSE +0 -21
- package/src/__tests__/inline-atom.test.tsx +0 -139
- package/src/useInlineAtom.ts +0 -40
package/dist/index.js
CHANGED
|
@@ -1,21 +1,30 @@
|
|
|
1
|
-
import { createContext, useContext, useSyncExternalStore, useCallback
|
|
2
|
-
import { getDefaultStore, command } from 'ccstate';
|
|
1
|
+
import { createContext, useContext, useRef, useSyncExternalStore, useCallback } from 'react';
|
|
3
2
|
|
|
4
3
|
var StoreContext = createContext(null);
|
|
5
4
|
var StoreProvider = StoreContext.Provider;
|
|
6
5
|
function useStore() {
|
|
7
6
|
var store = useContext(StoreContext);
|
|
8
7
|
if (!store) {
|
|
9
|
-
|
|
8
|
+
throw new Error('useStore must be used within a StoreProvider');
|
|
10
9
|
}
|
|
11
10
|
return store;
|
|
12
11
|
}
|
|
13
12
|
|
|
14
13
|
function useGet(atom) {
|
|
15
14
|
var store = useStore();
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
var onChange = useRef(function (fn) {
|
|
16
|
+
var controller = new AbortController();
|
|
17
|
+
store.watch(function (get) {
|
|
18
|
+
get(atom);
|
|
19
|
+
fn();
|
|
20
|
+
}, {
|
|
21
|
+
signal: controller.signal
|
|
22
|
+
});
|
|
23
|
+
return function () {
|
|
24
|
+
controller.abort();
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
return useSyncExternalStore(onChange.current, function () {
|
|
19
28
|
return store.get(atom);
|
|
20
29
|
});
|
|
21
30
|
}
|
|
@@ -32,132 +41,54 @@ function useSet(signal) {
|
|
|
32
41
|
}, [store, signal]);
|
|
33
42
|
}
|
|
34
43
|
|
|
35
|
-
function
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return n;
|
|
39
|
-
}
|
|
40
|
-
function _arrayWithHoles(r) {
|
|
41
|
-
if (Array.isArray(r)) return r;
|
|
42
|
-
}
|
|
43
|
-
function _iterableToArrayLimit(r, l) {
|
|
44
|
-
var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
|
|
45
|
-
if (null != t) {
|
|
46
|
-
var e,
|
|
47
|
-
n,
|
|
48
|
-
i,
|
|
49
|
-
u,
|
|
50
|
-
a = [],
|
|
51
|
-
f = !0,
|
|
52
|
-
o = !1;
|
|
53
|
-
try {
|
|
54
|
-
if (i = (t = t.call(r)).next, 0 === l) ; else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0);
|
|
55
|
-
} catch (r) {
|
|
56
|
-
o = !0, n = r;
|
|
57
|
-
} finally {
|
|
58
|
-
try {
|
|
59
|
-
if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return;
|
|
60
|
-
} finally {
|
|
61
|
-
if (o) throw n;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return a;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
function _nonIterableRest() {
|
|
68
|
-
throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
|
|
69
|
-
}
|
|
70
|
-
function _slicedToArray(r, e) {
|
|
71
|
-
return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();
|
|
72
|
-
}
|
|
73
|
-
function _unsupportedIterableToArray(r, a) {
|
|
74
|
-
if (r) {
|
|
75
|
-
if ("string" == typeof r) return _arrayLikeToArray(r, a);
|
|
76
|
-
var t = {}.toString.call(r).slice(8, -1);
|
|
77
|
-
return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
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;
|
|
44
|
+
function useLoadableInternal(promise$, keepLastResolved) {
|
|
45
|
+
var promiseResult = useRef({
|
|
46
|
+
state: 'loading'
|
|
112
47
|
});
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
_useGetPromise2 = _slicedToArray(_useGetPromise, 2),
|
|
120
|
-
promise = _useGetPromise2[0],
|
|
121
|
-
setPromiseProcessed = _useGetPromise2[1];
|
|
122
|
-
var _useState = useState({
|
|
123
|
-
state: 'loading'
|
|
124
|
-
}),
|
|
125
|
-
_useState2 = _slicedToArray(_useState, 2),
|
|
126
|
-
promiseResult = _useState2[0],
|
|
127
|
-
setPromiseResult = _useState2[1];
|
|
128
|
-
useEffect(function () {
|
|
129
|
-
if (!(promise instanceof Promise)) {
|
|
130
|
-
setPromiseResult({
|
|
131
|
-
state: 'hasData',
|
|
132
|
-
data: promise
|
|
133
|
-
});
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
var cancelled = false;
|
|
137
|
-
if (!keepLastResolved) {
|
|
138
|
-
setPromiseResult({
|
|
139
|
-
state: 'loading'
|
|
140
|
-
});
|
|
48
|
+
var store = useStore();
|
|
49
|
+
var subStore = useCallback(function (fn) {
|
|
50
|
+
function updateResult(result, signal) {
|
|
51
|
+
if (signal.aborted) return;
|
|
52
|
+
promiseResult.current = result;
|
|
53
|
+
fn();
|
|
141
54
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
55
|
+
var controller = new AbortController();
|
|
56
|
+
store.watch(function (get, _ref) {
|
|
57
|
+
var signal = _ref.signal;
|
|
58
|
+
var promise = get(promise$);
|
|
59
|
+
if (!(promise instanceof Promise)) {
|
|
60
|
+
updateResult({
|
|
61
|
+
state: 'hasData',
|
|
62
|
+
data: promise
|
|
63
|
+
}, signal);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (!keepLastResolved) {
|
|
67
|
+
updateResult({
|
|
68
|
+
state: 'loading'
|
|
69
|
+
}, signal);
|
|
70
|
+
}
|
|
71
|
+
promise.then(function (ret) {
|
|
72
|
+
updateResult({
|
|
73
|
+
state: 'hasData',
|
|
74
|
+
data: ret
|
|
75
|
+
}, signal);
|
|
76
|
+
}, function (error) {
|
|
77
|
+
updateResult({
|
|
78
|
+
state: 'hasError',
|
|
79
|
+
error: error
|
|
80
|
+
}, signal);
|
|
154
81
|
});
|
|
82
|
+
}, {
|
|
83
|
+
signal: controller.signal
|
|
155
84
|
});
|
|
156
85
|
return function () {
|
|
157
|
-
|
|
86
|
+
controller.abort();
|
|
158
87
|
};
|
|
159
|
-
}, [promise]);
|
|
160
|
-
return
|
|
88
|
+
}, [store, promise$]);
|
|
89
|
+
return useSyncExternalStore(subStore, function () {
|
|
90
|
+
return promiseResult.current;
|
|
91
|
+
});
|
|
161
92
|
}
|
|
162
93
|
function useLoadable(atom) {
|
|
163
94
|
return useLoadableInternal(atom, false);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccstate-react",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.2.0",
|
|
4
4
|
"description": "CCState React Hooks",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
},
|
|
9
9
|
"license": "MIT",
|
|
10
10
|
"type": "module",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "rollup -c",
|
|
13
|
+
"prebuild": "shx rm -rf dist"
|
|
14
|
+
},
|
|
11
15
|
"main": "./dist/index.cjs",
|
|
12
16
|
"module": "./dist/index.js",
|
|
13
17
|
"exports": {
|
|
@@ -25,7 +29,7 @@
|
|
|
25
29
|
"react": ">=17.0.0"
|
|
26
30
|
},
|
|
27
31
|
"dependencies": {
|
|
28
|
-
"ccstate": "
|
|
32
|
+
"ccstate": "workspace:^"
|
|
29
33
|
},
|
|
30
34
|
"peerDependenciesMeta": {
|
|
31
35
|
"@types/react": {
|
|
@@ -45,6 +49,7 @@
|
|
|
45
49
|
"@testing-library/user-event": "^14.5.2",
|
|
46
50
|
"@types/react": "^19.0.2",
|
|
47
51
|
"@types/react-dom": "^18.3.1",
|
|
52
|
+
"ccstate": "workspace:^",
|
|
48
53
|
"happy-dom": "^15.11.7",
|
|
49
54
|
"jest-leak-detector": "^29.7.0",
|
|
50
55
|
"json": "^11.0.0",
|
|
@@ -54,11 +59,6 @@
|
|
|
54
59
|
"rollup-plugin-dts": "^6.1.1",
|
|
55
60
|
"shx": "^0.3.4",
|
|
56
61
|
"signal-timers": "^1.0.4",
|
|
57
|
-
"vitest": "^2.1.8"
|
|
58
|
-
"ccstate": "^4.13.0"
|
|
59
|
-
},
|
|
60
|
-
"scripts": {
|
|
61
|
-
"build": "rollup -c",
|
|
62
|
-
"prebuild": "shx rm -rf dist"
|
|
62
|
+
"vitest": "^2.1.8"
|
|
63
63
|
}
|
|
64
|
-
}
|
|
64
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { render, cleanup, screen } from '@testing-library/react';
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
3
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
-
import { computed, createStore, command, state, createDebugStore
|
|
4
|
+
import { computed, createStore, command, state, createDebugStore } from 'ccstate';
|
|
5
5
|
import { StoreProvider, useGet, useSet } from '..';
|
|
6
6
|
import { StrictMode, useState } from 'react';
|
|
7
7
|
import '@testing-library/jest-dom/vitest';
|
|
@@ -198,21 +198,21 @@ describe('react', () => {
|
|
|
198
198
|
expect(await screen.findByText('1')).toBeInTheDocument();
|
|
199
199
|
});
|
|
200
200
|
|
|
201
|
-
it('
|
|
201
|
+
it('throw error if no store provide', () => {
|
|
202
202
|
const count$ = state(0);
|
|
203
|
-
getDefaultStore().set(count$, 10);
|
|
204
203
|
|
|
205
204
|
function App() {
|
|
206
205
|
const count = useGet(count$);
|
|
207
206
|
return <div>{count}</div>;
|
|
208
207
|
}
|
|
209
208
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
<
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
209
|
+
expect(() => {
|
|
210
|
+
render(
|
|
211
|
+
<StrictMode>
|
|
212
|
+
<App />
|
|
213
|
+
</StrictMode>,
|
|
214
|
+
);
|
|
215
|
+
}).toThrowError('useStore must be used within a StoreProvider');
|
|
216
216
|
});
|
|
217
217
|
|
|
218
218
|
it('will unmount when component cleanup', async () => {
|
|
@@ -221,7 +221,7 @@ describe('react', () => {
|
|
|
221
221
|
|
|
222
222
|
function App() {
|
|
223
223
|
const ret = useGet(base$);
|
|
224
|
-
return <div>{ret}</div>;
|
|
224
|
+
return <div>ret:{ret}</div>;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
function Container() {
|
|
@@ -252,12 +252,16 @@ describe('react', () => {
|
|
|
252
252
|
);
|
|
253
253
|
|
|
254
254
|
const user = userEvent.setup();
|
|
255
|
-
|
|
255
|
+
|
|
256
|
+
expect(screen.getByText('ret:0')).toBeInTheDocument();
|
|
256
257
|
const button = screen.getByText('hide');
|
|
258
|
+
|
|
259
|
+
expect(store.getReadDependents(base$)).toHaveLength(2);
|
|
260
|
+
|
|
257
261
|
expect(button).toBeInTheDocument();
|
|
258
262
|
await user.click(button);
|
|
259
263
|
expect(await screen.findByText('unmounted')).toBeInTheDocument();
|
|
260
|
-
expect(store.
|
|
264
|
+
expect(store.getReadDependents(base$)).toHaveLength(1);
|
|
261
265
|
});
|
|
262
266
|
});
|
|
263
267
|
|
|
@@ -287,7 +291,12 @@ it('useSet should be stable', () => {
|
|
|
287
291
|
return <div>Render</div>;
|
|
288
292
|
}
|
|
289
293
|
|
|
290
|
-
|
|
294
|
+
const store = createStore();
|
|
295
|
+
render(
|
|
296
|
+
<StoreProvider value={store}>
|
|
297
|
+
<Container />
|
|
298
|
+
</StoreProvider>,
|
|
299
|
+
);
|
|
291
300
|
|
|
292
301
|
expect(trace).toHaveBeenCalledTimes(2);
|
|
293
302
|
expect(trace.mock.calls[0][0]).toBe(trace.mock.calls[1][0]);
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import '@testing-library/jest-dom/vitest';
|
|
4
|
+
import { render, cleanup, screen } from '@testing-library/react';
|
|
5
|
+
import userEvent from '@testing-library/user-event';
|
|
6
|
+
import { afterEach, expect, it } from 'vitest';
|
|
7
|
+
import { delay } from 'signal-timers';
|
|
8
|
+
import { command, createStore, state } from 'ccstate';
|
|
9
|
+
import { StrictMode } from 'react';
|
|
10
|
+
import { StoreProvider } from '..';
|
|
11
|
+
import { useLoadableSet } from '../experimental';
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
cleanup();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function makeDefered<T>(): {
|
|
18
|
+
resolve: (value: T) => void;
|
|
19
|
+
reject: (error: unknown) => void;
|
|
20
|
+
promise: Promise<T>;
|
|
21
|
+
} {
|
|
22
|
+
const deferred: {
|
|
23
|
+
resolve: (value: T) => void;
|
|
24
|
+
reject: (error: unknown) => void;
|
|
25
|
+
promise: Promise<T>;
|
|
26
|
+
} = {} as {
|
|
27
|
+
resolve: (value: T) => void;
|
|
28
|
+
reject: (error: unknown) => void;
|
|
29
|
+
promise: Promise<T>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
deferred.promise = new Promise((resolve, reject) => {
|
|
33
|
+
deferred.resolve = resolve;
|
|
34
|
+
deferred.reject = reject;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return deferred;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
it('convert a async command to loadable', async () => {
|
|
41
|
+
const deferred = makeDefered<string>();
|
|
42
|
+
const setFoo$ = command(async () => {
|
|
43
|
+
return await deferred.promise;
|
|
44
|
+
});
|
|
45
|
+
const App = () => {
|
|
46
|
+
const [ret, setFoo] = useLoadableSet(setFoo$);
|
|
47
|
+
if (ret.state === 'loading') {
|
|
48
|
+
return <div>loading</div>;
|
|
49
|
+
} else if (ret.state === 'hasData') {
|
|
50
|
+
return <div>{ret.data}</div>;
|
|
51
|
+
}
|
|
52
|
+
return (
|
|
53
|
+
<button
|
|
54
|
+
onClick={() => {
|
|
55
|
+
void setFoo();
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
Click
|
|
59
|
+
</button>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
const store = createStore();
|
|
63
|
+
render(
|
|
64
|
+
<StoreProvider value={store}>
|
|
65
|
+
<App />
|
|
66
|
+
</StoreProvider>,
|
|
67
|
+
{ wrapper: StrictMode },
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
expect(screen.getByText('Click')).toBeTruthy();
|
|
71
|
+
|
|
72
|
+
const btn = await screen.findByText('Click');
|
|
73
|
+
await userEvent.click(btn);
|
|
74
|
+
|
|
75
|
+
expect(await screen.findByText('loading')).toBeTruthy();
|
|
76
|
+
|
|
77
|
+
deferred.resolve('foo');
|
|
78
|
+
expect(await screen.findByText('foo')).toBeTruthy();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('async command reject turns into hasError', async () => {
|
|
82
|
+
const deferred = makeDefered<string>();
|
|
83
|
+
const setFoo$ = command(async () => {
|
|
84
|
+
return await deferred.promise;
|
|
85
|
+
});
|
|
86
|
+
const App = () => {
|
|
87
|
+
const [ret, setFoo] = useLoadableSet(setFoo$);
|
|
88
|
+
if (ret.state === 'loading') {
|
|
89
|
+
return <div>loading</div>;
|
|
90
|
+
} else if (ret.state === 'hasError') {
|
|
91
|
+
return <div>{String(ret.error)}</div>;
|
|
92
|
+
}
|
|
93
|
+
return (
|
|
94
|
+
<button
|
|
95
|
+
onClick={() => {
|
|
96
|
+
void setFoo();
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
99
|
+
Click
|
|
100
|
+
</button>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
const store = createStore();
|
|
104
|
+
render(
|
|
105
|
+
<StoreProvider value={store}>
|
|
106
|
+
<App />
|
|
107
|
+
</StoreProvider>,
|
|
108
|
+
{ wrapper: StrictMode },
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
await userEvent.click(await screen.findByText('Click'));
|
|
112
|
+
expect(await screen.findByText('loading')).toBeTruthy();
|
|
113
|
+
|
|
114
|
+
deferred.reject(new Error('oops'));
|
|
115
|
+
expect(await screen.findByText('Error: oops')).toBeTruthy();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('sync command resolves immediately to hasData', async () => {
|
|
119
|
+
const setFoo$ = command(() => 42);
|
|
120
|
+
const App = () => {
|
|
121
|
+
const [ret, setFoo] = useLoadableSet(setFoo$);
|
|
122
|
+
if (ret.state === 'hasData') {
|
|
123
|
+
return <div>{String(ret.data)}</div>;
|
|
124
|
+
}
|
|
125
|
+
return (
|
|
126
|
+
<button
|
|
127
|
+
onClick={() => {
|
|
128
|
+
void setFoo();
|
|
129
|
+
}}
|
|
130
|
+
>
|
|
131
|
+
Click
|
|
132
|
+
</button>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
const store = createStore();
|
|
136
|
+
render(
|
|
137
|
+
<StoreProvider value={store}>
|
|
138
|
+
<App />
|
|
139
|
+
</StoreProvider>,
|
|
140
|
+
{ wrapper: StrictMode },
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
await userEvent.click(await screen.findByText('Click'));
|
|
144
|
+
expect(await screen.findByText('42')).toBeTruthy();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('state atom setter transitions to hasData', async () => {
|
|
148
|
+
const count$ = state(0);
|
|
149
|
+
const App = () => {
|
|
150
|
+
const [ret, setCount] = useLoadableSet(count$);
|
|
151
|
+
if (ret.state === 'hasData') {
|
|
152
|
+
return <div>done</div>;
|
|
153
|
+
}
|
|
154
|
+
return (
|
|
155
|
+
<button
|
|
156
|
+
onClick={() => {
|
|
157
|
+
setCount(1);
|
|
158
|
+
}}
|
|
159
|
+
>
|
|
160
|
+
Click
|
|
161
|
+
</button>
|
|
162
|
+
);
|
|
163
|
+
};
|
|
164
|
+
const store = createStore();
|
|
165
|
+
render(
|
|
166
|
+
<StoreProvider value={store}>
|
|
167
|
+
<App />
|
|
168
|
+
</StoreProvider>,
|
|
169
|
+
{ wrapper: StrictMode },
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
await userEvent.click(await screen.findByText('Click'));
|
|
173
|
+
expect(await screen.findByText('done')).toBeTruthy();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('second invoke cancels first pending promise', async () => {
|
|
177
|
+
const deferred1 = makeDefered<string>();
|
|
178
|
+
const deferred2 = makeDefered<string>();
|
|
179
|
+
let counter = 0;
|
|
180
|
+
|
|
181
|
+
const setFoo$ = command(async () => {
|
|
182
|
+
const deferred = counter === 0 ? deferred1 : deferred2;
|
|
183
|
+
counter++;
|
|
184
|
+
return await deferred.promise;
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const App = () => {
|
|
188
|
+
const [ret, setFoo] = useLoadableSet(setFoo$);
|
|
189
|
+
if (ret.state === 'hasData') {
|
|
190
|
+
return <div>data:{String(ret.data)}</div>;
|
|
191
|
+
}
|
|
192
|
+
return (
|
|
193
|
+
<button
|
|
194
|
+
onClick={() => {
|
|
195
|
+
void setFoo();
|
|
196
|
+
}}
|
|
197
|
+
>
|
|
198
|
+
Click
|
|
199
|
+
</button>
|
|
200
|
+
);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const store = createStore();
|
|
204
|
+
render(
|
|
205
|
+
<StoreProvider value={store}>
|
|
206
|
+
<App />
|
|
207
|
+
</StoreProvider>,
|
|
208
|
+
{ wrapper: StrictMode },
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
await userEvent.click(await screen.findByText('Click'));
|
|
212
|
+
// button still visible during loading — second invoke aborts the first
|
|
213
|
+
await userEvent.click(await screen.findByText('Click'));
|
|
214
|
+
|
|
215
|
+
deferred2.resolve('second');
|
|
216
|
+
expect(await screen.findByText('data:second')).toBeTruthy();
|
|
217
|
+
|
|
218
|
+
deferred1.resolve('first');
|
|
219
|
+
await delay(0);
|
|
220
|
+
expect(screen.queryByText('data:first')).toBeNull();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('invoke return value is identical to command return value', async () => {
|
|
224
|
+
const deferred = makeDefered<string>();
|
|
225
|
+
const setFoo$ = command(async () => {
|
|
226
|
+
return await deferred.promise;
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
let invokeResult: Promise<string> | undefined;
|
|
230
|
+
|
|
231
|
+
const App = () => {
|
|
232
|
+
const [, setFoo] = useLoadableSet(setFoo$);
|
|
233
|
+
return (
|
|
234
|
+
<button
|
|
235
|
+
onClick={() => {
|
|
236
|
+
invokeResult = setFoo();
|
|
237
|
+
}}
|
|
238
|
+
>
|
|
239
|
+
Click
|
|
240
|
+
</button>
|
|
241
|
+
);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const store = createStore();
|
|
245
|
+
render(
|
|
246
|
+
<StoreProvider value={store}>
|
|
247
|
+
<App />
|
|
248
|
+
</StoreProvider>,
|
|
249
|
+
{ wrapper: StrictMode },
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
await userEvent.click(await screen.findByText('Click'));
|
|
253
|
+
deferred.resolve('foo');
|
|
254
|
+
expect(await invokeResult).toBe('foo');
|
|
255
|
+
});
|
|
@@ -539,7 +539,12 @@ it('useLoadable accept sync computed', async () => {
|
|
|
539
539
|
return <div>{base.state}</div>;
|
|
540
540
|
}
|
|
541
541
|
|
|
542
|
-
|
|
542
|
+
const store = createStore();
|
|
543
|
+
render(
|
|
544
|
+
<StoreProvider value={store}>
|
|
545
|
+
<App />
|
|
546
|
+
</StoreProvider>,
|
|
547
|
+
);
|
|
543
548
|
|
|
544
549
|
expect(await screen.findByText('hasData')).toBeInTheDocument();
|
|
545
550
|
});
|
|
@@ -605,10 +610,10 @@ test('useLoadable should catch errors', () => {
|
|
|
605
610
|
get(reload$);
|
|
606
611
|
|
|
607
612
|
const p = Promise.resolve();
|
|
608
|
-
const
|
|
609
|
-
vi.spyOn(p, '
|
|
613
|
+
const originalThen = p.then.bind(p);
|
|
614
|
+
vi.spyOn(p, 'then').mockImplementation((...args) => {
|
|
610
615
|
traceCatch();
|
|
611
|
-
return
|
|
616
|
+
return originalThen(...args);
|
|
612
617
|
});
|
|
613
618
|
return p;
|
|
614
619
|
});
|
|
@@ -628,9 +633,12 @@ test('useLoadable should catch errors', () => {
|
|
|
628
633
|
</StoreProvider>
|
|
629
634
|
</StrictMode>,
|
|
630
635
|
);
|
|
636
|
+
expect(traceCatch).toHaveBeenCalledTimes(2); // strict mode renders twice
|
|
631
637
|
|
|
632
638
|
store.set(reload$, (x) => x + 1);
|
|
633
|
-
|
|
639
|
+
expect(traceCatch).toHaveBeenCalledTimes(3);
|
|
634
640
|
|
|
635
|
-
|
|
641
|
+
store.set(reload$, (x) => x + 1);
|
|
642
|
+
store.set(reload$, (x) => x + 1);
|
|
643
|
+
expect(traceCatch).toHaveBeenCalledTimes(5);
|
|
636
644
|
});
|
|
@@ -109,7 +109,12 @@ it('useResolved accept sync computed', async () => {
|
|
|
109
109
|
return <div>{base}</div>;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
const store = createStore();
|
|
113
|
+
render(
|
|
114
|
+
<StoreProvider value={store}>
|
|
115
|
+
<App />
|
|
116
|
+
</StoreProvider>,
|
|
117
|
+
);
|
|
113
118
|
|
|
114
119
|
expect(await screen.findByText('0')).toBeInTheDocument();
|
|
115
120
|
});
|
package/src/experimental.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { useLoadableSet } from './useLoadableSet';
|
package/src/provider.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { createContext, useContext } from 'react';
|
|
2
|
-
import { getDefaultStore } from 'ccstate';
|
|
3
2
|
import type { Store } from 'ccstate';
|
|
4
3
|
|
|
5
4
|
const StoreContext = createContext<Store | null>(null);
|
|
@@ -10,7 +9,7 @@ export function useStore(): Store {
|
|
|
10
9
|
const store = useContext(StoreContext);
|
|
11
10
|
|
|
12
11
|
if (!store) {
|
|
13
|
-
|
|
12
|
+
throw new Error('useStore must be used within a StoreProvider');
|
|
14
13
|
}
|
|
15
14
|
|
|
16
15
|
return store;
|