ccstate-react 5.0.0 → 5.2.1

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.
@@ -1,42 +1,109 @@
1
1
  'use strict';
2
2
 
3
- var ccstate = require('ccstate');
4
3
  var react = require('react');
5
4
 
6
- function useRefFactory(factory) {
7
- var ref = react.useRef(null);
8
- if (!ref.current) {
9
- var value = factory();
10
- ref.current = value;
11
- return value;
12
- }
13
- return ref.current;
5
+ function _arrayLikeToArray(r, a) {
6
+ (null == a || a > r.length) && (a = r.length);
7
+ for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
8
+ return n;
14
9
  }
15
- function useCCState() {
16
- for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
17
- args[_key] = arguments[_key];
18
- }
19
- return useRefFactory(function () {
20
- return ccstate.state.apply(void 0, args);
21
- });
10
+ function _arrayWithoutHoles(r) {
11
+ if (Array.isArray(r)) return _arrayLikeToArray(r);
12
+ }
13
+ function _iterableToArray(r) {
14
+ if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r);
15
+ }
16
+ function _nonIterableSpread() {
17
+ throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
18
+ }
19
+ function _toConsumableArray(r) {
20
+ return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread();
22
21
  }
23
- function useComputed() {
24
- for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
25
- args[_key2] = arguments[_key2];
22
+ function _unsupportedIterableToArray(r, a) {
23
+ if (r) {
24
+ if ("string" == typeof r) return _arrayLikeToArray(r, a);
25
+ var t = {}.toString.call(r).slice(8, -1);
26
+ 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;
26
27
  }
27
- return useRefFactory(function () {
28
- return ccstate.computed.apply(void 0, args);
29
- });
30
28
  }
31
- function useCommand() {
32
- for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
33
- args[_key3] = arguments[_key3];
29
+
30
+ var StoreContext = react.createContext(null);
31
+ StoreContext.Provider;
32
+ function useStore() {
33
+ var store = react.useContext(StoreContext);
34
+ if (!store) {
35
+ throw new Error('useStore must be used within a StoreProvider');
34
36
  }
35
- return useRefFactory(function () {
36
- return ccstate.command.apply(void 0, args);
37
+ return store;
38
+ }
39
+
40
+ function useLoadableSet(signal) {
41
+ var store = useStore();
42
+ var resultRef = react.useRef({
43
+ state: 'idle'
44
+ });
45
+ var notifyRef = react.useRef(null);
46
+ var controllerRef = react.useRef(null);
47
+ var subscribe = react.useCallback(function (notify) {
48
+ notifyRef.current = notify;
49
+ return function () {
50
+ var _controllerRef$curren;
51
+ notifyRef.current = null;
52
+ (_controllerRef$curren = controllerRef.current) === null || _controllerRef$curren === void 0 || _controllerRef$curren.abort();
53
+ };
54
+ }, []);
55
+ var invoke = react.useCallback(function () {
56
+ var _controllerRef$curren2;
57
+ (_controllerRef$curren2 = controllerRef.current) === null || _controllerRef$curren2 === void 0 || _controllerRef$curren2.abort();
58
+ var controller = new AbortController();
59
+ controllerRef.current = controller;
60
+ var abortSignal = controller.signal;
61
+ function updateResult(result) {
62
+ var _notifyRef$current;
63
+ if (abortSignal.aborted) return;
64
+ resultRef.current = result;
65
+ (_notifyRef$current = notifyRef.current) === null || _notifyRef$current === void 0 || _notifyRef$current.call(notifyRef);
66
+ }
67
+ for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
68
+ args[_key] = arguments[_key];
69
+ }
70
+ if ('write' in signal) {
71
+ var result = store.set.apply(store, [signal].concat(_toConsumableArray(args)));
72
+ if (result instanceof Promise) {
73
+ updateResult({
74
+ state: 'loading'
75
+ });
76
+ void result.then(function (data) {
77
+ updateResult({
78
+ state: 'hasData',
79
+ data: data
80
+ });
81
+ }, function (error) {
82
+ updateResult({
83
+ state: 'hasError',
84
+ error: error
85
+ });
86
+ });
87
+ } else {
88
+ updateResult({
89
+ state: 'hasData',
90
+ data: result
91
+ });
92
+ }
93
+ return result;
94
+ } else {
95
+ store.set.apply(store, [signal].concat(_toConsumableArray(args)));
96
+ updateResult({
97
+ state: 'hasData',
98
+ data: undefined
99
+ });
100
+ return undefined;
101
+ }
102
+ }, [store, signal]);
103
+ var loadable = react.useSyncExternalStore(subscribe, function () {
104
+ return resultRef.current;
37
105
  });
106
+ return [loadable, invoke];
38
107
  }
39
108
 
40
- exports.useCCState = useCCState;
41
- exports.useCommand = useCommand;
42
- exports.useComputed = useComputed;
109
+ exports.useLoadableSet = useLoadableSet;
@@ -1,7 +1,17 @@
1
- import { state, State, computed, Computed, command, Command } from 'ccstate';
1
+ import { State, StateArg, Command } from 'ccstate';
2
2
 
3
- declare function useCCState<T>(...args: Parameters<typeof state<T>>): State<T>;
4
- declare function useComputed<T>(...args: Parameters<typeof computed<T>>): Computed<T>;
5
- declare function useCommand<T, Args extends unknown[]>(...args: Parameters<typeof command<T, Args>>): Command<T, Args>;
3
+ type LoadableSetResult<T> = {
4
+ state: 'idle';
5
+ } | {
6
+ state: 'loading';
7
+ } | {
8
+ state: 'hasData';
9
+ data: Awaited<T>;
10
+ } | {
11
+ state: 'hasError';
12
+ error: unknown;
13
+ };
14
+ declare function useLoadableSet<T>(signal: State<T>): [LoadableSetResult<void>, (val: StateArg<T>) => void];
15
+ declare function useLoadableSet<T, CommandArgs extends unknown[]>(signal: Command<T, CommandArgs>): [LoadableSetResult<Awaited<T>>, (...args: CommandArgs) => T];
6
16
 
7
- export { useCCState, useCommand, useComputed };
17
+ export { useLoadableSet };
@@ -1,7 +1,17 @@
1
- import { state, State, computed, Computed, command, Command } from 'ccstate';
1
+ import { State, StateArg, Command } from 'ccstate';
2
2
 
3
- declare function useCCState<T>(...args: Parameters<typeof state<T>>): State<T>;
4
- declare function useComputed<T>(...args: Parameters<typeof computed<T>>): Computed<T>;
5
- declare function useCommand<T, Args extends unknown[]>(...args: Parameters<typeof command<T, Args>>): Command<T, Args>;
3
+ type LoadableSetResult<T> = {
4
+ state: 'idle';
5
+ } | {
6
+ state: 'loading';
7
+ } | {
8
+ state: 'hasData';
9
+ data: Awaited<T>;
10
+ } | {
11
+ state: 'hasError';
12
+ error: unknown;
13
+ };
14
+ declare function useLoadableSet<T>(signal: State<T>): [LoadableSetResult<void>, (val: StateArg<T>) => void];
15
+ declare function useLoadableSet<T, CommandArgs extends unknown[]>(signal: Command<T, CommandArgs>): [LoadableSetResult<Awaited<T>>, (...args: CommandArgs) => T];
6
16
 
7
- export { useCCState, useCommand, useComputed };
17
+ export { useLoadableSet };
@@ -1,38 +1,107 @@
1
- import { state, computed, command } from 'ccstate';
2
- import { useRef } from 'react';
1
+ import { createContext, useContext, useRef, useCallback, useSyncExternalStore } from 'react';
3
2
 
4
- function useRefFactory(factory) {
5
- var ref = useRef(null);
6
- if (!ref.current) {
7
- var value = factory();
8
- ref.current = value;
9
- return value;
10
- }
11
- return ref.current;
3
+ function _arrayLikeToArray(r, a) {
4
+ (null == a || a > r.length) && (a = r.length);
5
+ for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
6
+ return n;
12
7
  }
13
- function useCCState() {
14
- for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
15
- args[_key] = arguments[_key];
16
- }
17
- return useRefFactory(function () {
18
- return state.apply(void 0, args);
19
- });
8
+ function _arrayWithoutHoles(r) {
9
+ if (Array.isArray(r)) return _arrayLikeToArray(r);
10
+ }
11
+ function _iterableToArray(r) {
12
+ if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r);
13
+ }
14
+ function _nonIterableSpread() {
15
+ throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
16
+ }
17
+ function _toConsumableArray(r) {
18
+ return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread();
20
19
  }
21
- function useComputed() {
22
- for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
23
- args[_key2] = arguments[_key2];
20
+ function _unsupportedIterableToArray(r, a) {
21
+ if (r) {
22
+ if ("string" == typeof r) return _arrayLikeToArray(r, a);
23
+ var t = {}.toString.call(r).slice(8, -1);
24
+ 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;
24
25
  }
25
- return useRefFactory(function () {
26
- return computed.apply(void 0, args);
27
- });
28
26
  }
29
- function useCommand() {
30
- for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
31
- args[_key3] = arguments[_key3];
27
+
28
+ var StoreContext = createContext(null);
29
+ StoreContext.Provider;
30
+ function useStore() {
31
+ var store = useContext(StoreContext);
32
+ if (!store) {
33
+ throw new Error('useStore must be used within a StoreProvider');
32
34
  }
33
- return useRefFactory(function () {
34
- return command.apply(void 0, args);
35
+ return store;
36
+ }
37
+
38
+ function useLoadableSet(signal) {
39
+ var store = useStore();
40
+ var resultRef = useRef({
41
+ state: 'idle'
42
+ });
43
+ var notifyRef = useRef(null);
44
+ var controllerRef = useRef(null);
45
+ var subscribe = useCallback(function (notify) {
46
+ notifyRef.current = notify;
47
+ return function () {
48
+ var _controllerRef$curren;
49
+ notifyRef.current = null;
50
+ (_controllerRef$curren = controllerRef.current) === null || _controllerRef$curren === void 0 || _controllerRef$curren.abort();
51
+ };
52
+ }, []);
53
+ var invoke = useCallback(function () {
54
+ var _controllerRef$curren2;
55
+ (_controllerRef$curren2 = controllerRef.current) === null || _controllerRef$curren2 === void 0 || _controllerRef$curren2.abort();
56
+ var controller = new AbortController();
57
+ controllerRef.current = controller;
58
+ var abortSignal = controller.signal;
59
+ function updateResult(result) {
60
+ var _notifyRef$current;
61
+ if (abortSignal.aborted) return;
62
+ resultRef.current = result;
63
+ (_notifyRef$current = notifyRef.current) === null || _notifyRef$current === void 0 || _notifyRef$current.call(notifyRef);
64
+ }
65
+ for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
66
+ args[_key] = arguments[_key];
67
+ }
68
+ if ('write' in signal) {
69
+ var result = store.set.apply(store, [signal].concat(_toConsumableArray(args)));
70
+ if (result instanceof Promise) {
71
+ updateResult({
72
+ state: 'loading'
73
+ });
74
+ void result.then(function (data) {
75
+ updateResult({
76
+ state: 'hasData',
77
+ data: data
78
+ });
79
+ }, function (error) {
80
+ updateResult({
81
+ state: 'hasError',
82
+ error: error
83
+ });
84
+ });
85
+ } else {
86
+ updateResult({
87
+ state: 'hasData',
88
+ data: result
89
+ });
90
+ }
91
+ return result;
92
+ } else {
93
+ store.set.apply(store, [signal].concat(_toConsumableArray(args)));
94
+ updateResult({
95
+ state: 'hasData',
96
+ data: undefined
97
+ });
98
+ return undefined;
99
+ }
100
+ }, [store, signal]);
101
+ var loadable = useSyncExternalStore(subscribe, function () {
102
+ return resultRef.current;
35
103
  });
104
+ return [loadable, invoke];
36
105
  }
37
106
 
38
- export { useCCState, useCommand, useComputed };
107
+ export { useLoadableSet };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccstate-react",
3
- "version": "5.0.0",
3
+ "version": "5.2.1",
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": "^5.0.0"
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": "^5.0.0"
59
- },
60
- "scripts": {
61
- "build": "rollup -c",
62
- "prebuild": "shx rm -rf dist"
62
+ "vitest": "^2.1.8"
63
63
  }
64
- }
64
+ }
@@ -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
+ });
@@ -1 +1 @@
1
- export { useCCState, useComputed, useCommand } from './useInlineAtom';
1
+ export { useLoadableSet } from './useLoadableSet';