ccstate-react 4.12.0 → 5.0.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 CHANGED
@@ -1,5 +1,32 @@
1
1
  # ccstate-react
2
2
 
3
+ ## 5.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 2fdba09: feat: provide watch method to replace sub
8
+
9
+ ### Minor Changes
10
+
11
+ - 211f5b5: refactor: better promise rejection handling
12
+ - 52c52fd: refactor: remove defaultStore
13
+
14
+ ### Patch Changes
15
+
16
+ - Updated dependencies [2fdba09]
17
+ - Updated dependencies [52c52fd]
18
+ - ccstate@5.0.0
19
+
20
+ ## 4.13.0
21
+
22
+ ### Minor Changes
23
+
24
+ - 8f06427: refactor: avoid unhandledRejection in useLoadable
25
+
26
+ ### Patch Changes
27
+
28
+ - ccstate@4.13.0
29
+
3
30
  ## 4.12.0
4
31
 
5
32
  ### Minor Changes
@@ -3,16 +3,6 @@
3
3
  var ccstate = require('ccstate');
4
4
  var react = require('react');
5
5
 
6
- var StoreContext = react.createContext(null);
7
- StoreContext.Provider;
8
- function useStore() {
9
- var store = react.useContext(StoreContext);
10
- if (!store) {
11
- return ccstate.getDefaultStore();
12
- }
13
- return store;
14
- }
15
-
16
6
  function useRefFactory(factory) {
17
7
  var ref = react.useRef(null);
18
8
  if (!ref.current) {
@@ -46,17 +36,7 @@ function useCommand() {
46
36
  return ccstate.command.apply(void 0, args);
47
37
  });
48
38
  }
49
- function useSub() {
50
- for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) {
51
- args[_key4] = arguments[_key4];
52
- }
53
- var store = useStore();
54
- react.useEffect(function () {
55
- return store.sub.apply(store, args);
56
- }, []);
57
- }
58
39
 
59
40
  exports.useCCState = useCCState;
60
41
  exports.useCommand = useCommand;
61
42
  exports.useComputed = useComputed;
62
- exports.useSub = useSub;
@@ -1,8 +1,7 @@
1
- import { state, State, computed, Computed, command, Command, Subscribe } from 'ccstate';
1
+ import { state, State, computed, Computed, command, Command } from 'ccstate';
2
2
 
3
3
  declare function useCCState<T>(...args: Parameters<typeof state<T>>): State<T>;
4
4
  declare function useComputed<T>(...args: Parameters<typeof computed<T>>): Computed<T>;
5
5
  declare function useCommand<T, Args extends unknown[]>(...args: Parameters<typeof command<T, Args>>): Command<T, Args>;
6
- declare function useSub(...args: Parameters<Subscribe>): void;
7
6
 
8
- export { useCCState, useCommand, useComputed, useSub };
7
+ export { useCCState, useCommand, useComputed };
@@ -1,8 +1,7 @@
1
- import { state, State, computed, Computed, command, Command, Subscribe } from 'ccstate';
1
+ import { state, State, computed, Computed, command, Command } from 'ccstate';
2
2
 
3
3
  declare function useCCState<T>(...args: Parameters<typeof state<T>>): State<T>;
4
4
  declare function useComputed<T>(...args: Parameters<typeof computed<T>>): Computed<T>;
5
5
  declare function useCommand<T, Args extends unknown[]>(...args: Parameters<typeof command<T, Args>>): Command<T, Args>;
6
- declare function useSub(...args: Parameters<Subscribe>): void;
7
6
 
8
- export { useCCState, useCommand, useComputed, useSub };
7
+ export { useCCState, useCommand, useComputed };
@@ -1,15 +1,5 @@
1
- import { getDefaultStore, state, computed, command } from 'ccstate';
2
- import { createContext, useContext, useEffect, useRef } from 'react';
3
-
4
- var StoreContext = createContext(null);
5
- StoreContext.Provider;
6
- function useStore() {
7
- var store = useContext(StoreContext);
8
- if (!store) {
9
- return getDefaultStore();
10
- }
11
- return store;
12
- }
1
+ import { state, computed, command } from 'ccstate';
2
+ import { useRef } from 'react';
13
3
 
14
4
  function useRefFactory(factory) {
15
5
  var ref = useRef(null);
@@ -44,14 +34,5 @@ function useCommand() {
44
34
  return command.apply(void 0, args);
45
35
  });
46
36
  }
47
- function useSub() {
48
- for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) {
49
- args[_key4] = arguments[_key4];
50
- }
51
- var store = useStore();
52
- useEffect(function () {
53
- return store.sub.apply(store, args);
54
- }, []);
55
- }
56
37
 
57
- export { useCCState, useCommand, useComputed, useSub };
38
+ export { useCCState, useCommand, useComputed };
package/dist/index.cjs CHANGED
@@ -1,42 +1,33 @@
1
1
  'use strict';
2
2
 
3
3
  var react = require('react');
4
- var ccstate = require('ccstate');
5
4
 
6
5
  var StoreContext = react.createContext(null);
7
6
  var StoreProvider = StoreContext.Provider;
8
7
  function useStore() {
9
8
  var store = react.useContext(StoreContext);
10
9
  if (!store) {
11
- return ccstate.getDefaultStore();
10
+ throw new Error('useStore must be used within a StoreProvider');
12
11
  }
13
12
  return store;
14
13
  }
15
14
 
16
- function useGetInternal(atom, _ref) {
17
- var silenceUnhandleRejection = _ref.silenceUnhandleRejection;
15
+ function useGet(atom) {
18
16
  var store = useStore();
19
- return react.useSyncExternalStore(function (fn) {
20
- var ctrl = new AbortController();
21
- store.sub(atom, ccstate.command(fn), {
22
- signal: ctrl.signal
17
+ var onChange = react.useRef(function (fn) {
18
+ var controller = new AbortController();
19
+ store.watch(function (get) {
20
+ get(atom);
21
+ fn();
22
+ }, {
23
+ signal: controller.signal
23
24
  });
24
25
  return function () {
25
- ctrl.abort();
26
+ controller.abort();
26
27
  };
27
- }, function () {
28
- var val = store.get(atom);
29
- if (val instanceof Promise && silenceUnhandleRejection) {
30
- val["catch"](function () {
31
- return void 0;
32
- });
33
- }
34
- return val;
35
28
  });
36
- }
37
- function useGet(atom) {
38
- return useGetInternal(atom, {
39
- silenceUnhandleRejection: false
29
+ return react.useSyncExternalStore(onChange.current, function () {
30
+ return store.get(atom);
40
31
  });
41
32
  }
42
33
 
@@ -52,95 +43,54 @@ function useSet(signal) {
52
43
  }, [store, signal]);
53
44
  }
54
45
 
55
- function _arrayLikeToArray(r, a) {
56
- (null == a || a > r.length) && (a = r.length);
57
- for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
58
- return n;
59
- }
60
- function _arrayWithHoles(r) {
61
- if (Array.isArray(r)) return r;
62
- }
63
- function _iterableToArrayLimit(r, l) {
64
- var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
65
- if (null != t) {
66
- var e,
67
- n,
68
- i,
69
- u,
70
- a = [],
71
- f = !0,
72
- o = !1;
73
- try {
74
- 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);
75
- } catch (r) {
76
- o = !0, n = r;
77
- } finally {
78
- try {
79
- if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return;
80
- } finally {
81
- if (o) throw n;
82
- }
83
- }
84
- return a;
85
- }
86
- }
87
- function _nonIterableRest() {
88
- throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
89
- }
90
- function _slicedToArray(r, e) {
91
- return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();
92
- }
93
- function _unsupportedIterableToArray(r, a) {
94
- if (r) {
95
- if ("string" == typeof r) return _arrayLikeToArray(r, a);
96
- var t = {}.toString.call(r).slice(8, -1);
97
- 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;
98
- }
99
- }
100
-
101
- function useLoadableInternal(atom, keepLastResolved) {
102
- var promise = useGetInternal(atom, {
103
- silenceUnhandleRejection: true
46
+ function useLoadableInternal(promise$, keepLastResolved) {
47
+ var promiseResult = react.useRef({
48
+ state: 'loading'
104
49
  });
105
- var _useState = react.useState({
106
- state: 'loading'
107
- }),
108
- _useState2 = _slicedToArray(_useState, 2),
109
- promiseResult = _useState2[0],
110
- setPromiseResult = _useState2[1];
111
- react.useEffect(function () {
112
- if (!(promise instanceof Promise)) {
113
- setPromiseResult({
114
- state: 'hasData',
115
- data: promise
116
- });
117
- return;
118
- }
119
- var ctrl = new AbortController();
120
- var signal = ctrl.signal;
121
- if (!keepLastResolved) {
122
- setPromiseResult({
123
- state: 'loading'
124
- });
125
- }
126
- promise.then(function (ret) {
127
- if (signal.aborted) return;
128
- setPromiseResult({
129
- state: 'hasData',
130
- data: ret
131
- });
132
- }, function (error) {
50
+ var store = useStore();
51
+ var subStore = react.useCallback(function (fn) {
52
+ function updateResult(result, signal) {
133
53
  if (signal.aborted) return;
134
- setPromiseResult({
135
- state: 'hasError',
136
- error: error
54
+ promiseResult.current = result;
55
+ fn();
56
+ }
57
+ var controller = new AbortController();
58
+ store.watch(function (get, _ref) {
59
+ var signal = _ref.signal;
60
+ var promise = get(promise$);
61
+ if (!(promise instanceof Promise)) {
62
+ updateResult({
63
+ state: 'hasData',
64
+ data: promise
65
+ }, signal);
66
+ return;
67
+ }
68
+ if (!keepLastResolved) {
69
+ updateResult({
70
+ state: 'loading'
71
+ }, signal);
72
+ }
73
+ promise.then(function (ret) {
74
+ updateResult({
75
+ state: 'hasData',
76
+ data: ret
77
+ }, signal);
78
+ }, function (error) {
79
+ updateResult({
80
+ state: 'hasError',
81
+ error: error
82
+ }, signal);
137
83
  });
84
+ }, {
85
+ signal: controller.signal
138
86
  });
139
87
  return function () {
140
- ctrl.abort();
88
+ controller.abort();
141
89
  };
142
- }, [promise]);
143
- return promiseResult;
90
+ }, [store, promise$]);
91
+ return react.useSyncExternalStore(subStore, function () {
92
+ return promiseResult.current;
93
+ });
144
94
  }
145
95
  function useLoadable(atom) {
146
96
  return useLoadableInternal(atom, false);
package/dist/index.d.cts CHANGED
@@ -15,13 +15,13 @@ type Loadable<T> = {
15
15
  state: 'loading';
16
16
  } | {
17
17
  state: 'hasData';
18
- data: T;
18
+ data: Awaited<T>;
19
19
  } | {
20
20
  state: 'hasError';
21
21
  error: unknown;
22
22
  };
23
- declare function useLoadable<T>(atom: State<Promise<Awaited<T>> | Awaited<T>> | Computed<Promise<Awaited<T>> | Awaited<T>>): Loadable<Awaited<T>>;
24
- declare function useLastLoadable<T>(atom: State<Promise<Awaited<T>> | Awaited<T>> | Computed<Promise<Awaited<T>> | Awaited<T>>): Loadable<Awaited<T>>;
23
+ declare function useLoadable<T>(atom: State<Promise<Awaited<T>> | Awaited<T>> | Computed<Promise<Awaited<T>> | Awaited<T>>): Loadable<T>;
24
+ declare function useLastLoadable<T>(atom: State<Promise<Awaited<T>> | Awaited<T>> | Computed<Promise<Awaited<T>> | Awaited<T>>): Loadable<T>;
25
25
 
26
26
  declare const StoreProvider: react.Provider<Store | null>;
27
27
 
package/dist/index.d.ts CHANGED
@@ -15,13 +15,13 @@ type Loadable<T> = {
15
15
  state: 'loading';
16
16
  } | {
17
17
  state: 'hasData';
18
- data: T;
18
+ data: Awaited<T>;
19
19
  } | {
20
20
  state: 'hasError';
21
21
  error: unknown;
22
22
  };
23
- declare function useLoadable<T>(atom: State<Promise<Awaited<T>> | Awaited<T>> | Computed<Promise<Awaited<T>> | Awaited<T>>): Loadable<Awaited<T>>;
24
- declare function useLastLoadable<T>(atom: State<Promise<Awaited<T>> | Awaited<T>> | Computed<Promise<Awaited<T>> | Awaited<T>>): Loadable<Awaited<T>>;
23
+ declare function useLoadable<T>(atom: State<Promise<Awaited<T>> | Awaited<T>> | Computed<Promise<Awaited<T>> | Awaited<T>>): Loadable<T>;
24
+ declare function useLastLoadable<T>(atom: State<Promise<Awaited<T>> | Awaited<T>> | Computed<Promise<Awaited<T>> | Awaited<T>>): Loadable<T>;
25
25
 
26
26
  declare const StoreProvider: react.Provider<Store | null>;
27
27
 
package/dist/index.js CHANGED
@@ -1,40 +1,31 @@
1
- import { createContext, useContext, useSyncExternalStore, useCallback, useState, useEffect } from 'react';
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
- return getDefaultStore();
8
+ throw new Error('useStore must be used within a StoreProvider');
10
9
  }
11
10
  return store;
12
11
  }
13
12
 
14
- function useGetInternal(atom, _ref) {
15
- var silenceUnhandleRejection = _ref.silenceUnhandleRejection;
13
+ function useGet(atom) {
16
14
  var store = useStore();
17
- return useSyncExternalStore(function (fn) {
18
- var ctrl = new AbortController();
19
- store.sub(atom, command(fn), {
20
- signal: ctrl.signal
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
21
22
  });
22
23
  return function () {
23
- ctrl.abort();
24
+ controller.abort();
24
25
  };
25
- }, function () {
26
- var val = store.get(atom);
27
- if (val instanceof Promise && silenceUnhandleRejection) {
28
- val["catch"](function () {
29
- return void 0;
30
- });
31
- }
32
- return val;
33
26
  });
34
- }
35
- function useGet(atom) {
36
- return useGetInternal(atom, {
37
- silenceUnhandleRejection: false
27
+ return useSyncExternalStore(onChange.current, function () {
28
+ return store.get(atom);
38
29
  });
39
30
  }
40
31
 
@@ -50,95 +41,54 @@ function useSet(signal) {
50
41
  }, [store, signal]);
51
42
  }
52
43
 
53
- function _arrayLikeToArray(r, a) {
54
- (null == a || a > r.length) && (a = r.length);
55
- for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
56
- return n;
57
- }
58
- function _arrayWithHoles(r) {
59
- if (Array.isArray(r)) return r;
60
- }
61
- function _iterableToArrayLimit(r, l) {
62
- var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
63
- if (null != t) {
64
- var e,
65
- n,
66
- i,
67
- u,
68
- a = [],
69
- f = !0,
70
- o = !1;
71
- try {
72
- 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);
73
- } catch (r) {
74
- o = !0, n = r;
75
- } finally {
76
- try {
77
- if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return;
78
- } finally {
79
- if (o) throw n;
80
- }
81
- }
82
- return a;
83
- }
84
- }
85
- function _nonIterableRest() {
86
- throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
87
- }
88
- function _slicedToArray(r, e) {
89
- return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();
90
- }
91
- function _unsupportedIterableToArray(r, a) {
92
- if (r) {
93
- if ("string" == typeof r) return _arrayLikeToArray(r, a);
94
- var t = {}.toString.call(r).slice(8, -1);
95
- 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;
96
- }
97
- }
98
-
99
- function useLoadableInternal(atom, keepLastResolved) {
100
- var promise = useGetInternal(atom, {
101
- silenceUnhandleRejection: true
44
+ function useLoadableInternal(promise$, keepLastResolved) {
45
+ var promiseResult = useRef({
46
+ state: 'loading'
102
47
  });
103
- var _useState = useState({
104
- state: 'loading'
105
- }),
106
- _useState2 = _slicedToArray(_useState, 2),
107
- promiseResult = _useState2[0],
108
- setPromiseResult = _useState2[1];
109
- useEffect(function () {
110
- if (!(promise instanceof Promise)) {
111
- setPromiseResult({
112
- state: 'hasData',
113
- data: promise
114
- });
115
- return;
116
- }
117
- var ctrl = new AbortController();
118
- var signal = ctrl.signal;
119
- if (!keepLastResolved) {
120
- setPromiseResult({
121
- state: 'loading'
122
- });
123
- }
124
- promise.then(function (ret) {
125
- if (signal.aborted) return;
126
- setPromiseResult({
127
- state: 'hasData',
128
- data: ret
129
- });
130
- }, function (error) {
48
+ var store = useStore();
49
+ var subStore = useCallback(function (fn) {
50
+ function updateResult(result, signal) {
131
51
  if (signal.aborted) return;
132
- setPromiseResult({
133
- state: 'hasError',
134
- error: error
52
+ promiseResult.current = result;
53
+ fn();
54
+ }
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);
135
81
  });
82
+ }, {
83
+ signal: controller.signal
136
84
  });
137
85
  return function () {
138
- ctrl.abort();
86
+ controller.abort();
139
87
  };
140
- }, [promise]);
141
- return promiseResult;
88
+ }, [store, promise$]);
89
+ return useSyncExternalStore(subStore, function () {
90
+ return promiseResult.current;
91
+ });
142
92
  }
143
93
  function useLoadable(atom) {
144
94
  return useLoadableInternal(atom, false);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccstate-react",
3
- "version": "4.12.0",
3
+ "version": "5.0.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.12.0"
28
+ "ccstate": "^5.0.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.12.0"
58
+ "ccstate": "^5.0.0"
59
59
  },
60
60
  "scripts": {
61
61
  "build": "rollup -c",
@@ -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, getDefaultStore } from 'ccstate';
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('should use default store if no provider', () => {
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
- render(
211
- <StrictMode>
212
- <App />
213
- </StrictMode>,
214
- );
215
- expect(screen.getByText('10')).toBeInTheDocument();
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
- expect(store.getSubscribeGraph()).toHaveLength(1);
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.getSubscribeGraph()).toHaveLength(0);
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
- render(<Container />);
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]);
@@ -4,8 +4,8 @@ import '@testing-library/jest-dom/vitest';
4
4
  import { screen, cleanup, render } from '@testing-library/react';
5
5
  import { StrictMode } from 'react';
6
6
  import userEvent from '@testing-library/user-event';
7
- import { useCCState, useCommand, useComputed, useSub } from '../experimental';
8
- import { createDebugStore } from 'ccstate';
7
+ import { useCCState, useCommand, useComputed } from '../experimental';
8
+ import { createStore } from 'ccstate';
9
9
 
10
10
  afterEach(() => {
11
11
  cleanup();
@@ -62,9 +62,12 @@ it('use atom in React component', async () => {
62
62
  );
63
63
  }
64
64
 
65
+ const store = createStore();
65
66
  render(
66
67
  <StrictMode>
67
- <Counter />
68
+ <StoreProvider value={store}>
69
+ <Counter />
70
+ </StoreProvider>
68
71
  </StrictMode>,
69
72
  );
70
73
  expect(screen.getByText('count: 0')).toBeInTheDocument();
@@ -79,61 +82,3 @@ it('use atom in React component', async () => {
79
82
  expect(screen.getByText('count: 4')).toBeInTheDocument();
80
83
  expect(screen.getByText('double: 8')).toBeInTheDocument();
81
84
  });
82
-
83
- it('use sub in React component', async () => {
84
- function Counter() {
85
- const count$ = useCCState(0, {
86
- debugLabel: 'count$',
87
- });
88
- const double$ = useCCState(0, {
89
- debugLabel: 'double$',
90
- });
91
-
92
- const updateDouble$ = useCommand(
93
- ({ get, set }) => {
94
- const double = get(count$) * 2;
95
- set(double$, double);
96
- },
97
- {
98
- debugLabel: 'updateDouble$',
99
- },
100
- );
101
- useSub(count$, updateDouble$);
102
-
103
- const count = useGet(count$);
104
- const double = useGet(double$);
105
- const setCount = useSet(count$);
106
-
107
- return (
108
- <>
109
- <div>count: {String(count)}</div>
110
- <div>double: {String(double)}</div>
111
- <button
112
- onClick={() => {
113
- setCount((prev) => prev + 1);
114
- }}
115
- >
116
- Increment
117
- </button>
118
- </>
119
- );
120
- }
121
-
122
- const store = createDebugStore();
123
- render(
124
- <StrictMode>
125
- <StoreProvider value={store}>
126
- <Counter />
127
- </StoreProvider>
128
- </StrictMode>,
129
- );
130
- expect(screen.getByText('count: 0')).toBeInTheDocument();
131
-
132
- const button = screen.getByText('Increment');
133
- await user.click(button);
134
- expect(screen.getByText('count: 1')).toBeInTheDocument();
135
- expect(await screen.findByText('double: 2')).toBeInTheDocument();
136
-
137
- cleanup();
138
- expect(store.getSubscribeGraph()).toEqual([]);
139
- });
@@ -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';
@@ -539,7 +539,12 @@ it('useLoadable accept sync computed', async () => {
539
539
  return <div>{base.state}</div>;
540
540
  }
541
541
 
542
- render(<App />);
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
  });
@@ -583,9 +588,11 @@ describe('works with AbortError', () => {
583
588
  }
584
589
 
585
590
  render(
586
- <StoreProvider value={store}>
587
- <App />
588
- </StoreProvider>,
591
+ <StrictMode>
592
+ <StoreProvider value={store}>
593
+ <App />
594
+ </StoreProvider>
595
+ </StrictMode>,
589
596
  );
590
597
 
591
598
  expect(screen.getByText('Test')).toBeInTheDocument();
@@ -594,3 +601,44 @@ describe('works with AbortError', () => {
594
601
  store.set(reload$, (x) => x + 1);
595
602
  });
596
603
  });
604
+
605
+ test('useLoadable should catch errors', () => {
606
+ const reload$ = state(0);
607
+
608
+ const traceCatch = vi.fn();
609
+ const promise$ = computed((get) => {
610
+ get(reload$);
611
+
612
+ const p = Promise.resolve();
613
+ const originalThen = p.then.bind(p);
614
+ vi.spyOn(p, 'then').mockImplementation((...args) => {
615
+ traceCatch();
616
+ return originalThen(...args);
617
+ });
618
+ return p;
619
+ });
620
+
621
+ const store = createStore();
622
+
623
+ function App() {
624
+ useLoadable(promise$);
625
+
626
+ return null;
627
+ }
628
+
629
+ render(
630
+ <StrictMode>
631
+ <StoreProvider value={store}>
632
+ <App />
633
+ </StoreProvider>
634
+ </StrictMode>,
635
+ );
636
+ expect(traceCatch).toHaveBeenCalledTimes(2); // strict mode renders twice
637
+
638
+ store.set(reload$, (x) => x + 1);
639
+ expect(traceCatch).toHaveBeenCalledTimes(3);
640
+
641
+ store.set(reload$, (x) => x + 1);
642
+ store.set(reload$, (x) => x + 1);
643
+ expect(traceCatch).toHaveBeenCalledTimes(5);
644
+ });
@@ -109,7 +109,12 @@ it('useResolved accept sync computed', async () => {
109
109
  return <div>{base}</div>;
110
110
  }
111
111
 
112
- render(<App />);
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
  });
@@ -1 +1 @@
1
- export { useCCState, useComputed, useCommand, useSub } from './useInlineAtom';
1
+ export { useCCState, useComputed, useCommand } from './useInlineAtom';
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
- return getDefaultStore();
12
+ throw new Error('useStore must be used within a StoreProvider');
14
13
  }
15
14
 
16
15
  return store;
package/src/useGet.ts CHANGED
@@ -1,31 +1,25 @@
1
- import { useSyncExternalStore } from 'react';
1
+ import { useRef, useSyncExternalStore } from 'react';
2
2
  import { useStore } from './provider';
3
- import { command } from 'ccstate';
3
+
4
4
  import type { Computed, State } from 'ccstate';
5
5
 
6
- export function useGetInternal<T>(
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
- return useSyncExternalStore(
12
- (fn) => {
13
- const ctrl = new AbortController();
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
- },
26
- );
27
- }
8
+ const onChange = useRef((fn: () => void) => {
9
+ const controller = new AbortController();
10
+ store.watch(
11
+ (get) => {
12
+ get(atom);
13
+ fn();
14
+ },
15
+ {
16
+ signal: controller.signal,
17
+ },
18
+ );
19
+ return () => {
20
+ controller.abort();
21
+ };
22
+ });
28
23
 
29
- export function useGet<T>(atom: State<T> | Computed<T>) {
30
- return useGetInternal(atom, { silenceUnhandleRejection: false });
24
+ return useSyncExternalStore(onChange.current, () => store.get(atom));
31
25
  }
@@ -1,6 +1,5 @@
1
- import { command, computed, state, type Command, type Computed, type State, type Subscribe } from 'ccstate';
2
- import { useEffect, useRef } from 'react';
3
- import { useStore } from './provider';
1
+ import { command, computed, state, type Command, type Computed, type State } from 'ccstate';
2
+ import { useRef } from 'react';
4
3
 
5
4
  function useRefFactory<T>(factory: () => T): T {
6
5
  const ref = useRef<T | null>(null);
@@ -30,11 +29,3 @@ export function useCommand<T, Args extends unknown[]>(...args: Parameters<typeof
30
29
  return command(...args);
31
30
  });
32
31
  }
33
-
34
- export function useSub(...args: Parameters<Subscribe>) {
35
- const store = useStore();
36
-
37
- useEffect(() => {
38
- return store.sub(...args);
39
- }, []);
40
- }
@@ -1,6 +1,6 @@
1
- import { useEffect, useState } from 'react';
2
- import { useGetInternal } from './useGet';
3
- import type { Computed, State } from 'ccstate';
1
+ import { useCallback, useRef, useSyncExternalStore } from 'react';
2
+ import { type Computed, type State } from 'ccstate';
3
+ import { useStore } from './provider';
4
4
 
5
5
  type Loadable<T> =
6
6
  | {
@@ -8,79 +8,99 @@ type Loadable<T> =
8
8
  }
9
9
  | {
10
10
  state: 'hasData';
11
- data: T;
11
+ data: Awaited<T>;
12
12
  }
13
13
  | {
14
14
  state: 'hasError';
15
15
  error: unknown;
16
16
  };
17
17
 
18
- function useLoadableInternal<T>(
19
- atom: State<Promise<T> | T> | Computed<Promise<T> | T>,
18
+ function useLoadableInternal<T, U extends Promise<Awaited<T>> | Awaited<T>>(
19
+ promise$: State<U> | Computed<U>,
20
20
  keepLastResolved: boolean,
21
21
  ): Loadable<T> {
22
- const promise = useGetInternal(atom, {
23
- silenceUnhandleRejection: true,
24
- });
25
-
26
- const [promiseResult, setPromiseResult] = useState<Loadable<T>>({
22
+ const promiseResult = useRef<Loadable<T>>({
27
23
  state: 'loading',
28
24
  });
29
25
 
30
- useEffect(() => {
31
- if (!(promise instanceof Promise)) {
32
- setPromiseResult({
33
- state: 'hasData',
34
- data: promise,
35
- });
36
-
37
- return;
38
- }
39
-
40
- const ctrl = new AbortController();
41
- const signal = ctrl.signal;
26
+ const store = useStore();
27
+ const subStore = useCallback(
28
+ (fn: () => void) => {
29
+ function updateResult(result: Loadable<T>, signal: AbortSignal) {
30
+ if (signal.aborted) return;
31
+ promiseResult.current = result;
32
+ fn();
33
+ }
42
34
 
43
- if (!keepLastResolved) {
44
- setPromiseResult({
45
- state: 'loading',
46
- });
47
- }
35
+ const controller = new AbortController();
48
36
 
49
- promise.then(
50
- (ret) => {
51
- if (signal.aborted) return;
37
+ store.watch(
38
+ (get, { signal }) => {
39
+ const promise: Promise<Awaited<T>> | Awaited<T> = get(promise$);
40
+ if (!(promise instanceof Promise)) {
41
+ updateResult(
42
+ {
43
+ state: 'hasData',
44
+ data: promise,
45
+ },
46
+ signal,
47
+ );
48
+ return;
49
+ }
52
50
 
53
- setPromiseResult({
54
- state: 'hasData',
55
- data: ret,
56
- });
57
- },
58
- (error: unknown) => {
59
- if (signal.aborted) return;
51
+ if (!keepLastResolved) {
52
+ updateResult(
53
+ {
54
+ state: 'loading',
55
+ },
56
+ signal,
57
+ );
58
+ }
60
59
 
61
- setPromiseResult({
62
- state: 'hasError',
63
- error,
64
- });
65
- },
66
- );
60
+ promise.then(
61
+ (ret) => {
62
+ updateResult(
63
+ {
64
+ state: 'hasData',
65
+ data: ret,
66
+ },
67
+ signal,
68
+ );
69
+ },
70
+ (error: unknown) => {
71
+ updateResult(
72
+ {
73
+ state: 'hasError',
74
+ error,
75
+ },
76
+ signal,
77
+ );
78
+ },
79
+ );
80
+ },
81
+ {
82
+ signal: controller.signal,
83
+ },
84
+ );
67
85
 
68
- return () => {
69
- ctrl.abort();
70
- };
71
- }, [promise]);
86
+ return () => {
87
+ controller.abort();
88
+ };
89
+ },
90
+ [store, promise$],
91
+ );
72
92
 
73
- return promiseResult;
93
+ return useSyncExternalStore(subStore, () => promiseResult.current);
74
94
  }
75
95
 
76
96
  export function useLoadable<T>(
77
97
  atom: State<Promise<Awaited<T>> | Awaited<T>> | Computed<Promise<Awaited<T>> | Awaited<T>>,
78
- ): Loadable<Awaited<T>> {
98
+ ): Loadable<T> {
79
99
  return useLoadableInternal(atom, false);
80
100
  }
81
101
 
82
102
  export function useLastLoadable<T>(
83
103
  atom: State<Promise<Awaited<T>> | Awaited<T>> | Computed<Promise<Awaited<T>> | Awaited<T>>,
84
- ): Loadable<Awaited<T>> {
104
+ ): Loadable<T> {
85
105
  return useLoadableInternal(atom, true);
86
106
  }
@@ -4,13 +4,13 @@ import type { Computed, State } from 'ccstate';
4
4
  export function useResolved<T>(
5
5
  atom: State<Promise<Awaited<T>> | Awaited<T>> | Computed<Promise<Awaited<T>> | Awaited<T>>,
6
6
  ): Awaited<T> | undefined {
7
- const loadable = useLoadable(atom);
7
+ const loadable = useLoadable<T>(atom);
8
8
  return loadable.state === 'hasData' ? loadable.data : undefined;
9
9
  }
10
10
 
11
11
  export function useLastResolved<T>(
12
12
  atom: State<Promise<Awaited<T>> | Awaited<T>> | Computed<Promise<Awaited<T>> | Awaited<T>>,
13
13
  ): Awaited<T> | undefined {
14
- const loadable = useLastLoadable(atom);
14
+ const loadable = useLastLoadable<T>(atom);
15
15
  return loadable.state === 'hasData' ? loadable.data : undefined;
16
16
  }