ccstate 2.1.0 → 2.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/README.md +393 -157
- package/index.cjs +10 -32
- package/index.js +10 -32
- package/package.json +6 -2
- package/react/index.cjs +10 -32
- package/react/index.js +10 -32
- package/vue/index.cjs +71 -0
- package/vue/index.d.cts +53 -0
- package/vue/index.d.ts +53 -0
- package/vue/index.js +66 -0
package/README.md
CHANGED
|
@@ -10,8 +10,6 @@
|
|
|
10
10
|
[](https://codspeed.io/e7h4n/ccstate)
|
|
11
11
|
[](https://opensource.org/licenses/MIT)
|
|
12
12
|
|
|
13
|
-
English | [中文](README-zh.md)
|
|
14
|
-
|
|
15
13
|
CCState is a semantic, strict, and flexible state management library suitable for medium to large single-page applications with complex state management needs.
|
|
16
14
|
|
|
17
15
|
The name of CCState comes from three basic data types: computed, command, and state.
|
|
@@ -21,7 +19,7 @@ The name of CCState comes from three basic data types: computed, command, and st
|
|
|
21
19
|
- 💯 Simple & Intuitive: Crystal-clear API design with just 3 data types and 2 operations
|
|
22
20
|
- ✅ Rock-solid Reliability: Comprehensive test coverage reaching 100% branch coverage
|
|
23
21
|
- 🪶 Ultra-lightweight: Zero dependencies, only 500 lines of core code
|
|
24
|
-
- 💡 Framework Agnostic: Seamlessly works with React,
|
|
22
|
+
- 💡 Framework Agnostic: Seamlessly works with [React](docs/react.md), [Vue](docs/vue.md), or any UI framework
|
|
25
23
|
- 🚀 Blazing Fast: Optimized performance from day one, 2x-7x faster than Jotai across scenarios
|
|
26
24
|
|
|
27
25
|
## Getting Started
|
|
@@ -134,11 +132,14 @@ store.get(userId$); // 0
|
|
|
134
132
|
store.set(userId$, 100);
|
|
135
133
|
store.get(userId$); // 100
|
|
136
134
|
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
135
|
+
const user$ = state<({
|
|
136
|
+
name: 'e7h4n',
|
|
137
|
+
avatar: 'https://avatars.githubusercontent.com/u/813596',
|
|
138
|
+
} | undefined>(undefined);
|
|
139
|
+
store.set({
|
|
140
|
+
name: 'yc-kanyun',
|
|
141
|
+
avatar: 'https://avatars.githubusercontent.com/u/168416598'
|
|
140
142
|
});
|
|
141
|
-
store.get(callback$)(); // console log 'awesome ccstate'
|
|
142
143
|
```
|
|
143
144
|
|
|
144
145
|
These examples should be very easy to understand. You might notice a detail in the examples: all variables returned by `state` have a `$` suffix. This is a naming convention used to distinguish an CCState data type from other regular types. CCState data types must be accessed through the store's get/set methods, and since it's common to convert an CCState data type to a regular type using get, the `$` suffix helps avoid naming conflicts.
|
|
@@ -283,155 +284,11 @@ That's it! Next, you can learn how to use CCState in React.
|
|
|
283
284
|
|
|
284
285
|
## Using in React
|
|
285
286
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
```jsx
|
|
289
|
-
// main.tsx
|
|
290
|
-
import { createStore, StoreProvider } from 'ccstate';
|
|
291
|
-
import { App } from './App';
|
|
292
|
-
import { StrictMode } from 'react';
|
|
293
|
-
import { createRoot } from 'react-dom/client';
|
|
294
|
-
|
|
295
|
-
const store = createStore();
|
|
296
|
-
|
|
297
|
-
createRoot(document.getElementById('root')).render(
|
|
298
|
-
<StrictMode>
|
|
299
|
-
<StoreProvider value={store}>
|
|
300
|
-
<App />
|
|
301
|
-
</StoreProvider>
|
|
302
|
-
</StrictMode>,
|
|
303
|
-
);
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
All descendant components within the `StoreProvider` will use the provided store as the caller for `get` and `set` operations.
|
|
307
|
-
|
|
308
|
-
You can place the `StoreProvider` inside or outside of `StrictMode`; the functionality is the same.
|
|
309
|
-
|
|
310
|
-
### Retrieving Values
|
|
311
|
-
|
|
312
|
-
The most basic usage is to use `useGet` to retrieve the value from State or Computed.
|
|
313
|
-
|
|
314
|
-
```jsx
|
|
315
|
-
// data/count.ts
|
|
316
|
-
import { state } from 'ccstate';
|
|
317
|
-
export const count$ = state(0);
|
|
318
|
-
|
|
319
|
-
// App.tsx
|
|
320
|
-
import { useGet } from 'ccstate';
|
|
321
|
-
import { count$ } from './data/count';
|
|
322
|
-
|
|
323
|
-
function App() {
|
|
324
|
-
const count = useGet(count$);
|
|
325
|
-
return <div>{count}</div>;
|
|
326
|
-
}
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
`useGet` returns a `State` or a `Computed` value, and when the value changes, `useGet` triggers a re-render of the component.
|
|
330
|
-
|
|
331
|
-
`useGet` does not do anything special with `Promise` values. In fact, `useGet` is equivalent to a single `store.get` call, plus a `store.sub` to ensure reactive updates to the React component.
|
|
332
|
-
|
|
333
|
-
Two other useful hooks are available when dealing with `Promise` values. First, we introduce `useLoadable`.
|
|
334
|
-
|
|
335
|
-
```jsx
|
|
336
|
-
// data/user.ts
|
|
337
|
-
import { computed } from 'ccstate';
|
|
338
|
-
|
|
339
|
-
export const user$ = computed(async () => {
|
|
340
|
-
return fetch('/api/users/current').then((res) => res.json());
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
// App.tsx
|
|
344
|
-
import { useLoadable } from 'ccstate';
|
|
345
|
-
import { user$ } from './data/user';
|
|
346
|
-
|
|
347
|
-
function App() {
|
|
348
|
-
const user_ = useLoadable(user$);
|
|
349
|
-
if (user_.state === 'loading') return <div>Loading...</div>;
|
|
350
|
-
if (user_.state === 'error') return <div>Error: {user_.error.message}</div>;
|
|
351
|
-
|
|
352
|
-
return <div>{user_.data.name}</div>;
|
|
353
|
-
}
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
`useLoadable` accepts Value/Computed that returns a `Promise` and wraps the result in a `Loadable` structure.
|
|
357
|
-
|
|
358
|
-
```typescript
|
|
359
|
-
type Loadable<T> =
|
|
360
|
-
| {
|
|
361
|
-
state: 'loading';
|
|
362
|
-
}
|
|
363
|
-
| {
|
|
364
|
-
state: 'hasData';
|
|
365
|
-
data: T;
|
|
366
|
-
}
|
|
367
|
-
| {
|
|
368
|
-
state: 'hasError';
|
|
369
|
-
error: unknown;
|
|
370
|
-
};
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
This allows you to render loading and error states in JSX based on the state. `useLoadable` suppresses exceptions, so it will not trigger an `ErrorBoundary`.
|
|
374
|
-
|
|
375
|
-
Another useful hook is `useResolved`, which always returns the resolved value of a `Promise`.
|
|
376
|
-
|
|
377
|
-
```jsx
|
|
378
|
-
// App.tsx
|
|
379
|
-
import { useResolved } from 'ccstate';
|
|
380
|
-
import { user$ } from './data/user';
|
|
381
|
-
|
|
382
|
-
function App() {
|
|
383
|
-
const user = useResolved(user$);
|
|
384
|
-
return <div>{user?.name}</div>;
|
|
385
|
-
}
|
|
386
|
-
```
|
|
387
|
-
|
|
388
|
-
`useResolved` only returns the parameter passed to the resolve function so that it will return `undefined` during loading and when encountering error values. Like `useLoadable`, `useResolved` also suppresses exceptions. In fact, `useResolved` is a simple wrapper around `useLoadable`.
|
|
389
|
-
|
|
390
|
-
```typescript
|
|
391
|
-
// useResolved.ts
|
|
392
|
-
import { useLoadable } from './useLoadable';
|
|
393
|
-
import type { Computed, State } from '../core';
|
|
394
|
-
|
|
395
|
-
export function useResolved<T>(atom: State<Promise<T>> | Computed<Promise<T>>): T | undefined {
|
|
396
|
-
const loadable = useLoadable(atom);
|
|
397
|
-
return loadable.state === 'hasData' ? loadable.data : undefined;
|
|
398
|
-
}
|
|
399
|
-
```
|
|
400
|
-
|
|
401
|
-
### useLastLoadable & useLastResolved
|
|
402
|
-
|
|
403
|
-
In some scenarios, we want a refreshable Promise Computed to maintain its previous result during the refresh process instead of showing a loading state. CCState provides `useLastLoadable` and `useLastResolved` to achieve this functionality.
|
|
404
|
-
|
|
405
|
-
```jsx
|
|
406
|
-
import { useLoadable } from 'ccstate';
|
|
407
|
-
import { user$ } from './data/user';
|
|
408
|
-
|
|
409
|
-
function App() {
|
|
410
|
-
const user_ = useLastLoadable(user$); // Keep the previous result during new user$ request, without triggering loading state
|
|
411
|
-
if (user_.state === 'loading') return <div>Loading...</div>;
|
|
412
|
-
if (user_.state === 'error') return <div>Error: {user_.error.message}</div>;
|
|
287
|
+
[Using in React](docs/react.md)
|
|
413
288
|
|
|
414
|
-
|
|
415
|
-
}
|
|
416
|
-
```
|
|
417
|
-
|
|
418
|
-
`useLastResolved` behaves similarly - it always returns the last resolved value from a Promise Atom and won't reset to `undefined` when a new Promise is generated.
|
|
419
|
-
|
|
420
|
-
### Updating State / Triggering Command
|
|
421
|
-
|
|
422
|
-
The `useSet` hook can be used to update the value of State, or trigger Command. It returns a function equivalent to `store.set` when called.
|
|
423
|
-
|
|
424
|
-
```jsx
|
|
425
|
-
// App.tsx
|
|
426
|
-
import { useSet } from 'ccstate';
|
|
427
|
-
import { count$ } from './data/count';
|
|
289
|
+
## Using in Vue
|
|
428
290
|
|
|
429
|
-
|
|
430
|
-
const setCount = useSet(count$);
|
|
431
|
-
// setCount(x => x + 1) is equivalent to store.set(count$, x => x + 1)
|
|
432
|
-
return <button onClick={() => setCount((x) => x + 1)}>Increment</button>;
|
|
433
|
-
}
|
|
434
|
-
```
|
|
291
|
+
[Using in Vue](docs/vue.md)
|
|
435
292
|
|
|
436
293
|
### Testing & Debugging
|
|
437
294
|
|
|
@@ -472,13 +329,17 @@ store.sub(
|
|
|
472
329
|
|
|
473
330
|
## Concept behind CCState
|
|
474
331
|
|
|
475
|
-
CCState is inspired by Jotai.
|
|
332
|
+
CCState is inspired by Jotai. So everyone will ask questions: What's the ability of CCState that Jotai doesn't have?
|
|
333
|
+
|
|
334
|
+
The answer is: CCState intentionally has fewer features, simpler concepts, and less "magic" under the hood.
|
|
335
|
+
|
|
336
|
+
While Jotai is a great state management solution that has benefited the Motiff project significantly, as our project grew larger, especially with the increasing number of states (10k~100k atoms), we felt that some of Jotai's design choices needed adjustments, mainly in these aspects:
|
|
476
337
|
|
|
477
338
|
- Too many combinations of atom init/setter/getter methods, need simplification to reduce team's mental overhead
|
|
478
339
|
- Should reduce reactive capabilities, especially the `onMount` capability - the framework shouldn't provide this ability
|
|
479
340
|
- Some implicit magic operations, especially Promise wrapping, make the application execution process less transparent
|
|
480
341
|
|
|
481
|
-
To address these issues, I created CCState to express my thoughts on state management. Before detailing the differences from Jotai, we need to understand CCState's data types and subscription system.
|
|
342
|
+
To address these issues, I got an idea: "What concepts in Jotai are essential? And which concepts create mental overhead for developers?". Rather than just discussing it theoretically, I decided to try implementing it myself. So I created CCState to express my thoughts on state management. Before detailing the differences from Jotai, we need to understand CCState's data types and subscription system.
|
|
482
343
|
|
|
483
344
|
### More semantic data types
|
|
484
345
|
|
|
@@ -513,7 +374,39 @@ function setupPage() {
|
|
|
513
374
|
|
|
514
375
|
The consideration here is to avoid having callbacks depend on the Store object, which was a key design consideration when creating CCState. In CCState, `sub` is the only API with reactive capabilities, and CCState reduces the complexity of reactive computations by limiting Store usage.
|
|
515
376
|
|
|
516
|
-
|
|
377
|
+
In Jotai, there are no restrictions on writing code that uses sub within a sub callback:
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
store.sub(targetAtom, () => {
|
|
381
|
+
if (store.get(fooAtom)) {
|
|
382
|
+
store.sub(barAtom, () => {
|
|
383
|
+
// ...
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
In CCState, we can prevent this situation by moving the `Command` definition to a separate file and protecting the Store.
|
|
390
|
+
|
|
391
|
+
```typescript
|
|
392
|
+
// main.ts
|
|
393
|
+
import { callback$ } from './callbacks'
|
|
394
|
+
import { foo$ } from './states
|
|
395
|
+
|
|
396
|
+
function initApp() {
|
|
397
|
+
const store = createStore()
|
|
398
|
+
store.sub(foo$, callback$)
|
|
399
|
+
// do not expose store to outside
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// callbacks.ts
|
|
403
|
+
|
|
404
|
+
export const callback$ = command(({ get, set }) => {
|
|
405
|
+
// there is no way to use store sub
|
|
406
|
+
})
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
Therefore, in CCState, the capability of `sub` is intentionally limited. CCState encourages developers to handle data consistency updates within `Command`, rather than relying on subscription capabilities for reactive data updates. In fact, in a React application, CCState's `sub` is likely only used in conjunction with `useSyncExternalStore` to update views, while in all other scenarios, the code is completely moved into Commands.
|
|
517
410
|
|
|
518
411
|
### Avoid `useEffect` in React
|
|
519
412
|
|
|
@@ -581,6 +474,349 @@ root.render(
|
|
|
581
474
|
);
|
|
582
475
|
```
|
|
583
476
|
|
|
477
|
+
### Less Magic
|
|
478
|
+
|
|
479
|
+
#### No `onMount`: Maintaining Pure State Semantics
|
|
480
|
+
|
|
481
|
+
CCState intentionally omits `onMount` to preserve the side-effect-free nature of `Computed` and `State`. This design choice emphasizes clarity and predictability over convenience.
|
|
482
|
+
|
|
483
|
+
Let's examine a common pattern in Jotai and understand why CCState takes a different approach. [Consider the following scenario](https://codesandbox.io/p/sandbox/gkk43v):
|
|
484
|
+
|
|
485
|
+
```typescript
|
|
486
|
+
// atom.ts
|
|
487
|
+
const countAtom = atom(0);
|
|
488
|
+
countAtom.onMount = (setAtom) => {
|
|
489
|
+
const timer = setInterval(() => {
|
|
490
|
+
setAtom((x) => x + 1);
|
|
491
|
+
}, 1000);
|
|
492
|
+
return () => {
|
|
493
|
+
clearInterval(timer);
|
|
494
|
+
};
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
// App.tsx
|
|
498
|
+
function App() {
|
|
499
|
+
const count = useAtomValue(countAtom)
|
|
500
|
+
return <div>{count}</div>
|
|
501
|
+
}
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
It looks pretty cool, right? Just by using `useAtomValue` in React, you get an auto-incrementing timer. However, this means that subscribing to a `State` can potentially have side effects. Because it has side effects, we need to be very careful handling these side effects in scenarios like `useExternalStore` and `StrictMode`. In CCState, such timer auto-increment operations can only be placed in a `Command`.
|
|
505
|
+
|
|
506
|
+
```tsx
|
|
507
|
+
// logic.ts
|
|
508
|
+
export const count$ = state(0); // state is always effect-less
|
|
509
|
+
|
|
510
|
+
export const setupTimer$ = command(({ set }) => {
|
|
511
|
+
// command is considered to always have side effects
|
|
512
|
+
const timer = setInterval(() => {
|
|
513
|
+
set(count$, (x) => x + 1);
|
|
514
|
+
}, 1000);
|
|
515
|
+
return () => {
|
|
516
|
+
clearInterval(timer);
|
|
517
|
+
};
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// Must explicitly enable side effects in React
|
|
521
|
+
// App.tsx
|
|
522
|
+
function App() {
|
|
523
|
+
const count = useGet(count$);
|
|
524
|
+
const setupTimer = useSet(setupTimer$);
|
|
525
|
+
|
|
526
|
+
// Rendering App has side effects, so we explicitly enable them
|
|
527
|
+
useEffect(() => {
|
|
528
|
+
return setupTimer();
|
|
529
|
+
}, []);
|
|
530
|
+
|
|
531
|
+
return <div>{count}</div>;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// A more recommended approach is to enable side effects outside of React
|
|
535
|
+
// main.ts
|
|
536
|
+
store.sub(
|
|
537
|
+
// sub is always effect-less to any State
|
|
538
|
+
count$,
|
|
539
|
+
command(() => {
|
|
540
|
+
// ... onCount
|
|
541
|
+
}),
|
|
542
|
+
);
|
|
543
|
+
store.set(setupTimer$); // must setup effect explicitly
|
|
544
|
+
|
|
545
|
+
// ...
|
|
546
|
+
|
|
547
|
+
// The pure effect-less rendering process
|
|
548
|
+
root.render(function App() {
|
|
549
|
+
const count = useGet(count$);
|
|
550
|
+
|
|
551
|
+
return <div>{count}</div>;
|
|
552
|
+
});
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
I'm agree with [explicit is better than implicit](https://peps.python.org/pep-0020/), so CCState removes the `onMount` capability.
|
|
556
|
+
|
|
557
|
+
#### No `loadable` & `unwrap`
|
|
558
|
+
|
|
559
|
+
Jotai provides `loadable` and `unwrap` to handle Promise Atom, to convert them to a flat loading state atom. To implement this functionality, it inevitably needs to use `onMount` to subscribe to Promise changes and then modify its own return value.
|
|
560
|
+
|
|
561
|
+
As mentioned in the previous section, CCState does not provide `onMount`, so `loadable` and `unwrap` are neither present nor necessary in CCState. Instead, React hooks `useLoadable` and `useResolved` are provided as alternatives. The reason for this design is that I noticed a detail - only within a subscription system (like React's rendering part) do we need to convert a Promise into a loading state:
|
|
562
|
+
|
|
563
|
+
```tsx
|
|
564
|
+
// Jotai's example, since try/catch and async/await cannot be used in JSX, loadable is required to flatten the Promise
|
|
565
|
+
const userLoadableAtom = loadable(user$);
|
|
566
|
+
function User() {
|
|
567
|
+
const user = useAtomValue(userLoadableAtom);
|
|
568
|
+
if (user.state === 'loading') return <div>Loading...</div>;
|
|
569
|
+
if (user.state === 'error') return <div>Error: {user.error.message}</div>;
|
|
570
|
+
return <div>{user.data.name}</div>;
|
|
571
|
+
}
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
Or use loadable in the sub callback.
|
|
575
|
+
|
|
576
|
+
```ts
|
|
577
|
+
// Jotai's example
|
|
578
|
+
const userLoadableAtom = loadable(user$);
|
|
579
|
+
|
|
580
|
+
store.sub(userLoadableAtom, () => {
|
|
581
|
+
// Notice how similar this is to the JSX code above
|
|
582
|
+
const user = store.get(userLoadableAtom);
|
|
583
|
+
if (user.state === 'loading') return;
|
|
584
|
+
if (user.state === 'error') return;
|
|
585
|
+
|
|
586
|
+
// ...
|
|
587
|
+
});
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
CCState intentionally avoids overuse of the subscription pattern, encouraging developers to write state changes where they originate rather than where they are responded to.
|
|
591
|
+
|
|
592
|
+
```ts
|
|
593
|
+
// CCState's example, avoid use sub pattern to invoke effect
|
|
594
|
+
const updateUserId$ = command(({ set, get }) => {
|
|
595
|
+
// retrieve userId from somewhere
|
|
596
|
+
set(userId$, USER_ID)
|
|
597
|
+
|
|
598
|
+
set(connectRoom$)
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
const connectRoom$ = command({ set, get }) => {
|
|
602
|
+
const user = await get(user$)
|
|
603
|
+
// ... prepare connection for room
|
|
604
|
+
})
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
In React's subscription-based rendering system, I use `useEffect` to introduce subscription to Promises. The code below shows the actual implementation of `useLoadable`.
|
|
608
|
+
|
|
609
|
+
```ts
|
|
610
|
+
function useLastLoadable<T>(atom: State<Promise<T>> | Computed<Promise<T>>): Loadable<T> {
|
|
611
|
+
const promise = useGet(atom);
|
|
612
|
+
|
|
613
|
+
const [promiseResult, setPromiseResult] = useState<Loadable<T>>({
|
|
614
|
+
state: 'loading',
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
useEffect(() => {
|
|
618
|
+
const ctrl = new AbortController();
|
|
619
|
+
const signal = ctrl.signal;
|
|
620
|
+
|
|
621
|
+
void promise
|
|
622
|
+
.then((ret) => {
|
|
623
|
+
if (signal.aborted) return;
|
|
624
|
+
|
|
625
|
+
setPromiseResult({
|
|
626
|
+
state: 'hasData',
|
|
627
|
+
data: ret,
|
|
628
|
+
});
|
|
629
|
+
})
|
|
630
|
+
.catch((error: unknown) => {
|
|
631
|
+
// ...
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
return () => {
|
|
635
|
+
ctrl.abort();
|
|
636
|
+
};
|
|
637
|
+
}, [promise]);
|
|
638
|
+
|
|
639
|
+
return promiseResult;
|
|
640
|
+
}
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
Finally, CCState only implements Promise flattening in the React-related range, that's enough. By making these design choices, CCState maintains a cleaner separation of concerns, makes side effects more explicit, and reduces the overall complexity of state management. While this might require slightly more explicit code in some cases, it leads to more maintainable and predictable applications.
|
|
644
|
+
|
|
645
|
+
## Technical Details
|
|
646
|
+
|
|
647
|
+
### When Computed Values Are Evaluated
|
|
648
|
+
|
|
649
|
+
The execution of `read` function in `Computed` has several strategies:
|
|
650
|
+
|
|
651
|
+
1. If the `Computed` is not directly or indirectly subscribed, it only be evaluated when accessed by `get`
|
|
652
|
+
1. If the version number of other `Computed` | `State` accessed by the previous `read` is unchanged, use the result of the last `read` without re-evaluating it
|
|
653
|
+
2. Otherwise, re-evaluate `read` and mark its version number +1
|
|
654
|
+
2. Otherwise, if the `Computed` is directly or indirectly subscribed, it will constantly be re-evaluated when its dependency changes
|
|
655
|
+
|
|
656
|
+
I mentioned "directly or indirectly subscribed" twice. Here, we use a simpler term to express it. If a `Computed | Value` is directly or indirectly subscribed, we consider it to be _mounted_. Otherwise, it is deemed to be _unmounted_.
|
|
657
|
+
|
|
658
|
+
Consider this example:
|
|
659
|
+
|
|
660
|
+
```typescript
|
|
661
|
+
const base$ = state(0);
|
|
662
|
+
const branch$ = state('A');
|
|
663
|
+
const derived$ = computed((get) => {
|
|
664
|
+
if (get(branch$) !== 'B') {
|
|
665
|
+
return 0;
|
|
666
|
+
} else {
|
|
667
|
+
return get(base$) * 2;
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
In this example, `derived$` is not directly or indirectly subscribed, so it is always in the _unmounted_ state. At the same time, it has not been read, so it has no dependencies. At this point, resetting `base$` / `branch$` will not trigger the recomputation of `derived$`.
|
|
673
|
+
|
|
674
|
+
```
|
|
675
|
+
store.set(base$, 1) // will not trigger derived$'s read
|
|
676
|
+
store.set(branch$, 'C') // will not trigger derived$'s too
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
Once we read `derived$`, it will automatically record a dependency array.
|
|
680
|
+
|
|
681
|
+
```typescript
|
|
682
|
+
store.get(derived$); // return 0 because of branch$ === 'A'
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
At this point, the dependency array of `derived$` is `[branch$]`, because `derived$` did not access `base$` in the previous execution. Although CCState knows that `derived$` depends on `branch$`, because `branch$` is not mounted, the re-evaluation of `derived$` is lazy.
|
|
686
|
+
|
|
687
|
+
```typescript
|
|
688
|
+
store.set(branch$, 'D'); // will not trigger derived$'s read, until next get(derived$)
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
Once we mount `derived$` by `sub`, all its direct and indirect dependencies will enter the _mounted_ state.
|
|
692
|
+
|
|
693
|
+
```typescript
|
|
694
|
+
store.sub(
|
|
695
|
+
derived$,
|
|
696
|
+
command(() => void 0),
|
|
697
|
+
);
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
The mount graph in CCState is `[derived$, [branch$]]`. When `branch$` is reset, `derived$` will be re-evaluated immediately, and all subscribers will be notified.
|
|
701
|
+
|
|
702
|
+
```typescript
|
|
703
|
+
store.set(branch$, 'B'); // will trigger derived$'s read
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
In this re-evaluation, the dependency array of `derived$` is updated to `[branch$, base$]`, so `base$` will also be _mounted_. Any modification to `base$` will immediately trigger the re-evaluation of `derived$`.
|
|
707
|
+
|
|
708
|
+
```typescript
|
|
709
|
+
store.set(base$, 1); // will trigger derived$'s read and notify all subscribers
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
[Here's an example](https://codesandbox.io/p/sandbox/ds6p44). Open preview in an independent window to check the console output. If you hide the double output and click increment, you will only see the `set` log.
|
|
713
|
+
|
|
714
|
+
```
|
|
715
|
+
[R][SET] V1:count$
|
|
716
|
+
arg: – [function] (1)
|
|
717
|
+
ret: – undefined
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
Click show to make double enter the display state, and you can see the `set` `showDouble$` log and the `double$` evaluation log.
|
|
721
|
+
|
|
722
|
+
```
|
|
723
|
+
[R][SET] V0:showDouble$
|
|
724
|
+
arg: – [function] (1)
|
|
725
|
+
ret: – undefined
|
|
726
|
+
|
|
727
|
+
[R][CPT] C2:doubleCount$
|
|
728
|
+
ret: – 14
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
The abbreviation `CPT` represents `Computed` evaluation, not just a simple read operation. You can also try modifying the parameters of `createConsoleDebugStore` in the code to include `get` in the logs, and you'll find that not every `get` triggers a `Computed` evaluation.
|
|
732
|
+
|
|
733
|
+
Click increment to see the `set` trigger the `Computed` evaluation.
|
|
734
|
+
|
|
735
|
+
```
|
|
736
|
+
[R][SET] V1:count$
|
|
737
|
+
arg: – [function] (1)
|
|
738
|
+
[R][CPT] C2:doubleCount$
|
|
739
|
+
ret: – 16
|
|
740
|
+
ret: – undefined
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
### How to Isolate Effect-less Code
|
|
744
|
+
|
|
745
|
+
CCState strives to isolate effect-less code through API capability restrictions and thus introduces two accessor APIs: `get` and `set`. I remember when I first saw Jotai, I raised a question: why can't we directly use the Atom itself to read and write state, just like signals do?
|
|
746
|
+
|
|
747
|
+
Most state libraries allow you to directly read and write state once you get the state object:
|
|
748
|
+
|
|
749
|
+
```typescript
|
|
750
|
+
// Zustand
|
|
751
|
+
const useStore = create((set) => {
|
|
752
|
+
return {
|
|
753
|
+
count: 0,
|
|
754
|
+
updateCount: () => {
|
|
755
|
+
set({
|
|
756
|
+
count: (x) => x + 1,
|
|
757
|
+
});
|
|
758
|
+
},
|
|
759
|
+
};
|
|
760
|
+
});
|
|
761
|
+
useStore.getState().count; // read count is effect-less
|
|
762
|
+
useStore.getState().updateCount(); // update count invoke effect
|
|
763
|
+
|
|
764
|
+
// RxJS
|
|
765
|
+
const count$ = new BehaviorSubject(0);
|
|
766
|
+
count$.value; // read count is effect-less
|
|
767
|
+
count$.next(1); // next count invoke effect
|
|
768
|
+
|
|
769
|
+
// Signals
|
|
770
|
+
const counter = signal(0);
|
|
771
|
+
counter.value; // read value is effect-less
|
|
772
|
+
counter.value = 1; // write value invoke effect
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
So, these libraries cannot isolate effect-less code. Jotai and CCState choose to add a wrapper layer to isolate effect-less code.
|
|
776
|
+
|
|
777
|
+
```typescript
|
|
778
|
+
const count$ = state(0);
|
|
779
|
+
const double$ = computed((get) => {
|
|
780
|
+
get(count$); // read count$ is effect-less
|
|
781
|
+
// In this scope, we can't update any state
|
|
782
|
+
});
|
|
783
|
+
const updateDouble$ = command(({ get, set }) => {
|
|
784
|
+
// This scope can update the state because it has `set` method
|
|
785
|
+
set(count$, get(count$) * 2);
|
|
786
|
+
});
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
Isolating effect-less code is very useful in large projects, but is there a more straightforward way to write it? For example, a straightforward idea is to mark the current state of the `Store` as read-only when entering the `Computed` code block and then restore it to writable when exiting. In read-only mode, all `set` operations are blocked.
|
|
790
|
+
|
|
791
|
+
```typescript
|
|
792
|
+
const counter = state(0);
|
|
793
|
+
const double = computed(() => {
|
|
794
|
+
// set store to read-only
|
|
795
|
+
const result = counter.value * 2; // direct read value from counter instead of get(counter)
|
|
796
|
+
// counter.value = 4; // any write operation in read-only mode will raise an error
|
|
797
|
+
return result;
|
|
798
|
+
}); // exit computed restore store to writable
|
|
799
|
+
|
|
800
|
+
double.value; // will enter read-only mode, evaluate double logic, get the result, and exit read-only mode
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
Unfortunately, this design will fail when encountering asynchronous callback functions in the current JavaScript language capabilities.
|
|
804
|
+
|
|
805
|
+
```typescript
|
|
806
|
+
const double = computed(async () => {
|
|
807
|
+
// set store to read-only
|
|
808
|
+
await delay(TIME_TO_DELAY);
|
|
809
|
+
// How to restore the store to read-only here?
|
|
810
|
+
// ...
|
|
811
|
+
});
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
When encountering `await`, the execution of `double.value` will end, and the framework code in `Computed` can restore the `Store` to writable. If we don't do this, the subsequent set operation will raise an error. But if we do this, when `await` re-enters the `double` read function, it will not be able to restore the `Store` to read-only.
|
|
815
|
+
|
|
816
|
+
Now, we are in the execution context that persists across async tasks; we hope to restore the Store's context to read-only when the async callback re-enters the `read` function. This direction has been proven to have many problems by [zone.js](https://github.com/angular/angular/tree/main/packages/zone.js). This is a dead end.
|
|
817
|
+
|
|
818
|
+
So, I think the only way to implement `Computed`'s effect-less is to separate the atom and the accessor.
|
|
819
|
+
|
|
584
820
|
## Changelog & TODO
|
|
585
821
|
|
|
586
822
|
[Changelog](packages/ccstate/CHANGELOG.md)
|
package/index.cjs
CHANGED
|
@@ -1529,7 +1529,7 @@ function useSet(atom) {
|
|
|
1529
1529
|
};
|
|
1530
1530
|
}
|
|
1531
1531
|
|
|
1532
|
-
function
|
|
1532
|
+
function useLoadableInternal(atom, keepLastResolved) {
|
|
1533
1533
|
var promise = useGet(atom);
|
|
1534
1534
|
var _useState = react.useState({
|
|
1535
1535
|
state: 'loading'
|
|
@@ -1540,39 +1540,11 @@ function useLoadable(atom) {
|
|
|
1540
1540
|
react.useEffect(function () {
|
|
1541
1541
|
var ctrl = new AbortController();
|
|
1542
1542
|
var signal = ctrl.signal;
|
|
1543
|
-
|
|
1544
|
-
state: 'loading'
|
|
1545
|
-
});
|
|
1546
|
-
void promise.then(function (ret) {
|
|
1547
|
-
if (signal.aborted) return;
|
|
1548
|
-
setPromiseResult({
|
|
1549
|
-
state: 'hasData',
|
|
1550
|
-
data: ret
|
|
1551
|
-
});
|
|
1552
|
-
})["catch"](function (error) {
|
|
1553
|
-
if (signal.aborted) return;
|
|
1543
|
+
if (!keepLastResolved) {
|
|
1554
1544
|
setPromiseResult({
|
|
1555
|
-
state: '
|
|
1556
|
-
error: error
|
|
1545
|
+
state: 'loading'
|
|
1557
1546
|
});
|
|
1558
|
-
}
|
|
1559
|
-
return function () {
|
|
1560
|
-
ctrl.abort();
|
|
1561
|
-
};
|
|
1562
|
-
}, [promise]);
|
|
1563
|
-
return promiseResult;
|
|
1564
|
-
}
|
|
1565
|
-
function useLastLoadable(atom) {
|
|
1566
|
-
var promise = useGet(atom);
|
|
1567
|
-
var _useState3 = react.useState({
|
|
1568
|
-
state: 'loading'
|
|
1569
|
-
}),
|
|
1570
|
-
_useState4 = _slicedToArray(_useState3, 2),
|
|
1571
|
-
promiseResult = _useState4[0],
|
|
1572
|
-
setPromiseResult = _useState4[1];
|
|
1573
|
-
react.useEffect(function () {
|
|
1574
|
-
var ctrl = new AbortController();
|
|
1575
|
-
var signal = ctrl.signal;
|
|
1547
|
+
}
|
|
1576
1548
|
void promise.then(function (ret) {
|
|
1577
1549
|
if (signal.aborted) return;
|
|
1578
1550
|
setPromiseResult({
|
|
@@ -1592,6 +1564,12 @@ function useLastLoadable(atom) {
|
|
|
1592
1564
|
}, [promise]);
|
|
1593
1565
|
return promiseResult;
|
|
1594
1566
|
}
|
|
1567
|
+
function useLoadable(atom) {
|
|
1568
|
+
return useLoadableInternal(atom, false);
|
|
1569
|
+
}
|
|
1570
|
+
function useLastLoadable(atom) {
|
|
1571
|
+
return useLoadableInternal(atom, true);
|
|
1572
|
+
}
|
|
1595
1573
|
|
|
1596
1574
|
function useResolved(atom) {
|
|
1597
1575
|
var loadable = useLoadable(atom);
|
package/index.js
CHANGED
|
@@ -1527,7 +1527,7 @@ function useSet(atom) {
|
|
|
1527
1527
|
};
|
|
1528
1528
|
}
|
|
1529
1529
|
|
|
1530
|
-
function
|
|
1530
|
+
function useLoadableInternal(atom, keepLastResolved) {
|
|
1531
1531
|
var promise = useGet(atom);
|
|
1532
1532
|
var _useState = useState({
|
|
1533
1533
|
state: 'loading'
|
|
@@ -1538,39 +1538,11 @@ function useLoadable(atom) {
|
|
|
1538
1538
|
useEffect(function () {
|
|
1539
1539
|
var ctrl = new AbortController();
|
|
1540
1540
|
var signal = ctrl.signal;
|
|
1541
|
-
|
|
1542
|
-
state: 'loading'
|
|
1543
|
-
});
|
|
1544
|
-
void promise.then(function (ret) {
|
|
1545
|
-
if (signal.aborted) return;
|
|
1546
|
-
setPromiseResult({
|
|
1547
|
-
state: 'hasData',
|
|
1548
|
-
data: ret
|
|
1549
|
-
});
|
|
1550
|
-
})["catch"](function (error) {
|
|
1551
|
-
if (signal.aborted) return;
|
|
1541
|
+
if (!keepLastResolved) {
|
|
1552
1542
|
setPromiseResult({
|
|
1553
|
-
state: '
|
|
1554
|
-
error: error
|
|
1543
|
+
state: 'loading'
|
|
1555
1544
|
});
|
|
1556
|
-
}
|
|
1557
|
-
return function () {
|
|
1558
|
-
ctrl.abort();
|
|
1559
|
-
};
|
|
1560
|
-
}, [promise]);
|
|
1561
|
-
return promiseResult;
|
|
1562
|
-
}
|
|
1563
|
-
function useLastLoadable(atom) {
|
|
1564
|
-
var promise = useGet(atom);
|
|
1565
|
-
var _useState3 = useState({
|
|
1566
|
-
state: 'loading'
|
|
1567
|
-
}),
|
|
1568
|
-
_useState4 = _slicedToArray(_useState3, 2),
|
|
1569
|
-
promiseResult = _useState4[0],
|
|
1570
|
-
setPromiseResult = _useState4[1];
|
|
1571
|
-
useEffect(function () {
|
|
1572
|
-
var ctrl = new AbortController();
|
|
1573
|
-
var signal = ctrl.signal;
|
|
1545
|
+
}
|
|
1574
1546
|
void promise.then(function (ret) {
|
|
1575
1547
|
if (signal.aborted) return;
|
|
1576
1548
|
setPromiseResult({
|
|
@@ -1590,6 +1562,12 @@ function useLastLoadable(atom) {
|
|
|
1590
1562
|
}, [promise]);
|
|
1591
1563
|
return promiseResult;
|
|
1592
1564
|
}
|
|
1565
|
+
function useLoadable(atom) {
|
|
1566
|
+
return useLoadableInternal(atom, false);
|
|
1567
|
+
}
|
|
1568
|
+
function useLastLoadable(atom) {
|
|
1569
|
+
return useLoadableInternal(atom, true);
|
|
1570
|
+
}
|
|
1593
1571
|
|
|
1594
1572
|
function useResolved(atom) {
|
|
1595
1573
|
var loadable = useLoadable(atom);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccstate",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "CCState Core",
|
|
5
5
|
"private": false,
|
|
6
6
|
"repository": {
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
25
|
"@types/react": ">=17.0.0",
|
|
26
|
-
"react": ">=17.0.0"
|
|
26
|
+
"react": ">=17.0.0",
|
|
27
|
+
"vue": ">=3.2.0"
|
|
27
28
|
},
|
|
28
29
|
"peerDependenciesMeta": {
|
|
29
30
|
"@types/react": {
|
|
@@ -31,6 +32,9 @@
|
|
|
31
32
|
},
|
|
32
33
|
"react": {
|
|
33
34
|
"optional": true
|
|
35
|
+
},
|
|
36
|
+
"vue": {
|
|
37
|
+
"optional": true
|
|
34
38
|
}
|
|
35
39
|
}
|
|
36
40
|
}
|
package/react/index.cjs
CHANGED
|
@@ -105,7 +105,7 @@ function useSet(atom) {
|
|
|
105
105
|
};
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
function
|
|
108
|
+
function useLoadableInternal(atom, keepLastResolved) {
|
|
109
109
|
var promise = useGet(atom);
|
|
110
110
|
var _useState = react.useState({
|
|
111
111
|
state: 'loading'
|
|
@@ -116,39 +116,11 @@ function useLoadable(atom) {
|
|
|
116
116
|
react.useEffect(function () {
|
|
117
117
|
var ctrl = new AbortController();
|
|
118
118
|
var signal = ctrl.signal;
|
|
119
|
-
|
|
120
|
-
state: 'loading'
|
|
121
|
-
});
|
|
122
|
-
void promise.then(function (ret) {
|
|
123
|
-
if (signal.aborted) return;
|
|
124
|
-
setPromiseResult({
|
|
125
|
-
state: 'hasData',
|
|
126
|
-
data: ret
|
|
127
|
-
});
|
|
128
|
-
})["catch"](function (error) {
|
|
129
|
-
if (signal.aborted) return;
|
|
119
|
+
if (!keepLastResolved) {
|
|
130
120
|
setPromiseResult({
|
|
131
|
-
state: '
|
|
132
|
-
error: error
|
|
121
|
+
state: 'loading'
|
|
133
122
|
});
|
|
134
|
-
}
|
|
135
|
-
return function () {
|
|
136
|
-
ctrl.abort();
|
|
137
|
-
};
|
|
138
|
-
}, [promise]);
|
|
139
|
-
return promiseResult;
|
|
140
|
-
}
|
|
141
|
-
function useLastLoadable(atom) {
|
|
142
|
-
var promise = useGet(atom);
|
|
143
|
-
var _useState3 = react.useState({
|
|
144
|
-
state: 'loading'
|
|
145
|
-
}),
|
|
146
|
-
_useState4 = _slicedToArray(_useState3, 2),
|
|
147
|
-
promiseResult = _useState4[0],
|
|
148
|
-
setPromiseResult = _useState4[1];
|
|
149
|
-
react.useEffect(function () {
|
|
150
|
-
var ctrl = new AbortController();
|
|
151
|
-
var signal = ctrl.signal;
|
|
123
|
+
}
|
|
152
124
|
void promise.then(function (ret) {
|
|
153
125
|
if (signal.aborted) return;
|
|
154
126
|
setPromiseResult({
|
|
@@ -168,6 +140,12 @@ function useLastLoadable(atom) {
|
|
|
168
140
|
}, [promise]);
|
|
169
141
|
return promiseResult;
|
|
170
142
|
}
|
|
143
|
+
function useLoadable(atom) {
|
|
144
|
+
return useLoadableInternal(atom, false);
|
|
145
|
+
}
|
|
146
|
+
function useLastLoadable(atom) {
|
|
147
|
+
return useLoadableInternal(atom, true);
|
|
148
|
+
}
|
|
171
149
|
|
|
172
150
|
function useResolved(atom) {
|
|
173
151
|
var loadable = useLoadable(atom);
|
package/react/index.js
CHANGED
|
@@ -103,7 +103,7 @@ function useSet(atom) {
|
|
|
103
103
|
};
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
function
|
|
106
|
+
function useLoadableInternal(atom, keepLastResolved) {
|
|
107
107
|
var promise = useGet(atom);
|
|
108
108
|
var _useState = useState({
|
|
109
109
|
state: 'loading'
|
|
@@ -114,39 +114,11 @@ function useLoadable(atom) {
|
|
|
114
114
|
useEffect(function () {
|
|
115
115
|
var ctrl = new AbortController();
|
|
116
116
|
var signal = ctrl.signal;
|
|
117
|
-
|
|
118
|
-
state: 'loading'
|
|
119
|
-
});
|
|
120
|
-
void promise.then(function (ret) {
|
|
121
|
-
if (signal.aborted) return;
|
|
122
|
-
setPromiseResult({
|
|
123
|
-
state: 'hasData',
|
|
124
|
-
data: ret
|
|
125
|
-
});
|
|
126
|
-
})["catch"](function (error) {
|
|
127
|
-
if (signal.aborted) return;
|
|
117
|
+
if (!keepLastResolved) {
|
|
128
118
|
setPromiseResult({
|
|
129
|
-
state: '
|
|
130
|
-
error: error
|
|
119
|
+
state: 'loading'
|
|
131
120
|
});
|
|
132
|
-
}
|
|
133
|
-
return function () {
|
|
134
|
-
ctrl.abort();
|
|
135
|
-
};
|
|
136
|
-
}, [promise]);
|
|
137
|
-
return promiseResult;
|
|
138
|
-
}
|
|
139
|
-
function useLastLoadable(atom) {
|
|
140
|
-
var promise = useGet(atom);
|
|
141
|
-
var _useState3 = useState({
|
|
142
|
-
state: 'loading'
|
|
143
|
-
}),
|
|
144
|
-
_useState4 = _slicedToArray(_useState3, 2),
|
|
145
|
-
promiseResult = _useState4[0],
|
|
146
|
-
setPromiseResult = _useState4[1];
|
|
147
|
-
useEffect(function () {
|
|
148
|
-
var ctrl = new AbortController();
|
|
149
|
-
var signal = ctrl.signal;
|
|
121
|
+
}
|
|
150
122
|
void promise.then(function (ret) {
|
|
151
123
|
if (signal.aborted) return;
|
|
152
124
|
setPromiseResult({
|
|
@@ -166,6 +138,12 @@ function useLastLoadable(atom) {
|
|
|
166
138
|
}, [promise]);
|
|
167
139
|
return promiseResult;
|
|
168
140
|
}
|
|
141
|
+
function useLoadable(atom) {
|
|
142
|
+
return useLoadableInternal(atom, false);
|
|
143
|
+
}
|
|
144
|
+
function useLastLoadable(atom) {
|
|
145
|
+
return useLoadableInternal(atom, true);
|
|
146
|
+
}
|
|
169
147
|
|
|
170
148
|
function useResolved(atom) {
|
|
171
149
|
var loadable = useLoadable(atom);
|
package/vue/index.cjs
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var vue = require('vue');
|
|
4
|
+
|
|
5
|
+
var StoreKey = Symbol('ccstate-vue-store');
|
|
6
|
+
var provideStore = function provideStore(store) {
|
|
7
|
+
vue.provide(StoreKey, store);
|
|
8
|
+
};
|
|
9
|
+
var useStore = function useStore() {
|
|
10
|
+
var store = vue.inject(StoreKey);
|
|
11
|
+
if (store === undefined) {
|
|
12
|
+
throw new Error('Store context not found - did you forget to wrap your app with StoreProvider?');
|
|
13
|
+
}
|
|
14
|
+
return store;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
var globalId = 0;
|
|
18
|
+
var generateToString = function generateToString(prefix, debugLabel) {
|
|
19
|
+
var id = globalId++;
|
|
20
|
+
var label = "".concat(prefix).concat(String(id)).concat('');
|
|
21
|
+
return function () {
|
|
22
|
+
return label;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
function command(write, options) {
|
|
26
|
+
var ret = {
|
|
27
|
+
write: write,
|
|
28
|
+
toString: generateToString('F')
|
|
29
|
+
};
|
|
30
|
+
return ret;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function useGet(atom) {
|
|
34
|
+
var store = useStore();
|
|
35
|
+
var initialValue = store.get(atom);
|
|
36
|
+
var vueState = vue.shallowRef(initialValue);
|
|
37
|
+
var controller = new AbortController();
|
|
38
|
+
store.sub(atom, command(function () {
|
|
39
|
+
var nextValue = store.get(atom);
|
|
40
|
+
vueState.value = nextValue;
|
|
41
|
+
}), {
|
|
42
|
+
signal: controller.signal
|
|
43
|
+
});
|
|
44
|
+
if (vue.getCurrentInstance()) {
|
|
45
|
+
vue.onScopeDispose(function () {
|
|
46
|
+
controller.abort();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return vue.shallowReadonly(vueState);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function useSet(atom) {
|
|
53
|
+
var store = useStore();
|
|
54
|
+
if ('write' in atom) {
|
|
55
|
+
return function () {
|
|
56
|
+
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
|
|
57
|
+
args[_key] = arguments[_key];
|
|
58
|
+
}
|
|
59
|
+
var ret = store.set.apply(store, [atom].concat(args));
|
|
60
|
+
return ret;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return function (value) {
|
|
64
|
+
store.set(atom, value);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
exports.provideStore = provideStore;
|
|
69
|
+
exports.useGet = useGet;
|
|
70
|
+
exports.useSet = useSet;
|
|
71
|
+
exports.useStore = useStore;
|
package/vue/index.d.cts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ShallowRef } from 'vue';
|
|
2
|
+
|
|
3
|
+
type Updater<T> = (current: T) => T;
|
|
4
|
+
interface Setter {
|
|
5
|
+
<T>(state: State<T>, val: T | Updater<T>): void;
|
|
6
|
+
<T, Args extends unknown[]>(command: Command<T, Args>, ...args: Args): T;
|
|
7
|
+
}
|
|
8
|
+
type Getter = <T>(readable: ReadableAtom<T>) => T;
|
|
9
|
+
interface GetterOptions {
|
|
10
|
+
signal: AbortSignal;
|
|
11
|
+
}
|
|
12
|
+
type Read<T> = (get: Getter, options: GetterOptions) => T;
|
|
13
|
+
type Write<T, Args extends unknown[]> = (visitor: {
|
|
14
|
+
get: Getter;
|
|
15
|
+
set: Setter;
|
|
16
|
+
}, ...args: Args) => T;
|
|
17
|
+
interface State<T> {
|
|
18
|
+
init: T;
|
|
19
|
+
debugLabel?: string;
|
|
20
|
+
toString: () => string;
|
|
21
|
+
}
|
|
22
|
+
interface Computed<T> {
|
|
23
|
+
read: Read<T>;
|
|
24
|
+
debugLabel?: string;
|
|
25
|
+
toString: () => string;
|
|
26
|
+
}
|
|
27
|
+
interface Command<T, Args extends unknown[]> {
|
|
28
|
+
write: Write<T, Args>;
|
|
29
|
+
debugLabel?: string;
|
|
30
|
+
toString: () => string;
|
|
31
|
+
}
|
|
32
|
+
type ReadableAtom<T> = State<T> | Computed<T>;
|
|
33
|
+
|
|
34
|
+
interface Store {
|
|
35
|
+
get: Getter;
|
|
36
|
+
set: Setter;
|
|
37
|
+
sub: Subscribe;
|
|
38
|
+
}
|
|
39
|
+
interface SubscribeOptions {
|
|
40
|
+
signal?: AbortSignal;
|
|
41
|
+
}
|
|
42
|
+
type CallbackFunc<T> = Command<T, []>;
|
|
43
|
+
type Subscribe = (atoms$: ReadableAtom<unknown>[] | ReadableAtom<unknown>, callback: CallbackFunc<unknown>, options?: SubscribeOptions) => () => void;
|
|
44
|
+
|
|
45
|
+
declare const provideStore: (store: Store) => void;
|
|
46
|
+
declare const useStore: () => Store;
|
|
47
|
+
|
|
48
|
+
declare function useGet<Value>(atom: Computed<Value> | State<Value>): Readonly<ShallowRef<Value>>;
|
|
49
|
+
|
|
50
|
+
declare function useSet<T>(atom: State<T>): (value: T | Updater<T>) => void;
|
|
51
|
+
declare function useSet<T, ARGS extends unknown[]>(atom: Command<T, ARGS>): (...args: ARGS) => T;
|
|
52
|
+
|
|
53
|
+
export { provideStore, useGet, useSet, useStore };
|
package/vue/index.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ShallowRef } from 'vue';
|
|
2
|
+
|
|
3
|
+
type Updater<T> = (current: T) => T;
|
|
4
|
+
interface Setter {
|
|
5
|
+
<T>(state: State<T>, val: T | Updater<T>): void;
|
|
6
|
+
<T, Args extends unknown[]>(command: Command<T, Args>, ...args: Args): T;
|
|
7
|
+
}
|
|
8
|
+
type Getter = <T>(readable: ReadableAtom<T>) => T;
|
|
9
|
+
interface GetterOptions {
|
|
10
|
+
signal: AbortSignal;
|
|
11
|
+
}
|
|
12
|
+
type Read<T> = (get: Getter, options: GetterOptions) => T;
|
|
13
|
+
type Write<T, Args extends unknown[]> = (visitor: {
|
|
14
|
+
get: Getter;
|
|
15
|
+
set: Setter;
|
|
16
|
+
}, ...args: Args) => T;
|
|
17
|
+
interface State<T> {
|
|
18
|
+
init: T;
|
|
19
|
+
debugLabel?: string;
|
|
20
|
+
toString: () => string;
|
|
21
|
+
}
|
|
22
|
+
interface Computed<T> {
|
|
23
|
+
read: Read<T>;
|
|
24
|
+
debugLabel?: string;
|
|
25
|
+
toString: () => string;
|
|
26
|
+
}
|
|
27
|
+
interface Command<T, Args extends unknown[]> {
|
|
28
|
+
write: Write<T, Args>;
|
|
29
|
+
debugLabel?: string;
|
|
30
|
+
toString: () => string;
|
|
31
|
+
}
|
|
32
|
+
type ReadableAtom<T> = State<T> | Computed<T>;
|
|
33
|
+
|
|
34
|
+
interface Store {
|
|
35
|
+
get: Getter;
|
|
36
|
+
set: Setter;
|
|
37
|
+
sub: Subscribe;
|
|
38
|
+
}
|
|
39
|
+
interface SubscribeOptions {
|
|
40
|
+
signal?: AbortSignal;
|
|
41
|
+
}
|
|
42
|
+
type CallbackFunc<T> = Command<T, []>;
|
|
43
|
+
type Subscribe = (atoms$: ReadableAtom<unknown>[] | ReadableAtom<unknown>, callback: CallbackFunc<unknown>, options?: SubscribeOptions) => () => void;
|
|
44
|
+
|
|
45
|
+
declare const provideStore: (store: Store) => void;
|
|
46
|
+
declare const useStore: () => Store;
|
|
47
|
+
|
|
48
|
+
declare function useGet<Value>(atom: Computed<Value> | State<Value>): Readonly<ShallowRef<Value>>;
|
|
49
|
+
|
|
50
|
+
declare function useSet<T>(atom: State<T>): (value: T | Updater<T>) => void;
|
|
51
|
+
declare function useSet<T, ARGS extends unknown[]>(atom: Command<T, ARGS>): (...args: ARGS) => T;
|
|
52
|
+
|
|
53
|
+
export { provideStore, useGet, useSet, useStore };
|
package/vue/index.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { provide, inject, shallowRef, getCurrentInstance, onScopeDispose, shallowReadonly } from 'vue';
|
|
2
|
+
|
|
3
|
+
var StoreKey = Symbol('ccstate-vue-store');
|
|
4
|
+
var provideStore = function provideStore(store) {
|
|
5
|
+
provide(StoreKey, store);
|
|
6
|
+
};
|
|
7
|
+
var useStore = function useStore() {
|
|
8
|
+
var store = inject(StoreKey);
|
|
9
|
+
if (store === undefined) {
|
|
10
|
+
throw new Error('Store context not found - did you forget to wrap your app with StoreProvider?');
|
|
11
|
+
}
|
|
12
|
+
return store;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
var globalId = 0;
|
|
16
|
+
var generateToString = function generateToString(prefix, debugLabel) {
|
|
17
|
+
var id = globalId++;
|
|
18
|
+
var label = "".concat(prefix).concat(String(id)).concat('');
|
|
19
|
+
return function () {
|
|
20
|
+
return label;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
function command(write, options) {
|
|
24
|
+
var ret = {
|
|
25
|
+
write: write,
|
|
26
|
+
toString: generateToString('F')
|
|
27
|
+
};
|
|
28
|
+
return ret;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function useGet(atom) {
|
|
32
|
+
var store = useStore();
|
|
33
|
+
var initialValue = store.get(atom);
|
|
34
|
+
var vueState = shallowRef(initialValue);
|
|
35
|
+
var controller = new AbortController();
|
|
36
|
+
store.sub(atom, command(function () {
|
|
37
|
+
var nextValue = store.get(atom);
|
|
38
|
+
vueState.value = nextValue;
|
|
39
|
+
}), {
|
|
40
|
+
signal: controller.signal
|
|
41
|
+
});
|
|
42
|
+
if (getCurrentInstance()) {
|
|
43
|
+
onScopeDispose(function () {
|
|
44
|
+
controller.abort();
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return shallowReadonly(vueState);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function useSet(atom) {
|
|
51
|
+
var store = useStore();
|
|
52
|
+
if ('write' in atom) {
|
|
53
|
+
return function () {
|
|
54
|
+
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
|
|
55
|
+
args[_key] = arguments[_key];
|
|
56
|
+
}
|
|
57
|
+
var ret = store.set.apply(store, [atom].concat(args));
|
|
58
|
+
return ret;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return function (value) {
|
|
62
|
+
store.set(atom, value);
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { provideStore, useGet, useSet, useStore };
|