ccstate 2.0.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 CHANGED
@@ -1,4 +1,6 @@
1
- # CCState
1
+ <img src="https://github.com/user-attachments/assets/590797c8-6edf-45cc-8eae-028aef0b2cb3" width="240" >
2
+
3
+ ---
2
4
 
3
5
  [![Coverage Status](https://coveralls.io/repos/github/e7h4n/ccstate/badge.svg?branch=main)](https://coveralls.io/github/e7h4n/ccstate?branch=main)
4
6
  ![NPM Type Definitions](https://img.shields.io/npm/types/ccstate)
@@ -10,15 +12,15 @@
10
12
 
11
13
  CCState is a semantic, strict, and flexible state management library suitable for medium to large single-page applications with complex state management needs.
12
14
 
13
- The name CCState comes from its three basic data types: Computed, Command and State.
15
+ The name of CCState comes from three basic data types: computed, command, and state.
14
16
 
15
17
  ## Quick Features
16
18
 
17
- - Simple API design with only 3 data types and 2 data operations
18
- - Strict test coverage with 100% branch coverage
19
- - Zero dependencies
20
- - Not bound to any UI library - can be used with React or Vanilla JS
21
- - High Performance
19
+ - ðŸ’Ŋ Simple & Intuitive: Crystal-clear API design with just 3 data types and 2 operations
20
+ - ✅ Rock-solid Reliability: Comprehensive test coverage reaching 100% branch coverage
21
+ - ðŸŠķ Ultra-lightweight: Zero dependencies, only 500 lines of core code
22
+ - ðŸ’Ą Framework Agnostic: Seamlessly works with [React](docs/react.md), [Vue](docs/vue.md), or any UI framework
23
+ - 🚀 Blazing Fast: Optimized performance from day one, 2x-7x faster than Jotai across scenarios
22
24
 
23
25
  ## Getting Started
24
26
 
@@ -35,12 +37,12 @@ pnpm add ccstate
35
37
  yarn add ccstate
36
38
  ```
37
39
 
38
- ### Create Atoms
40
+ ### Create Data
39
41
 
40
- Use `state` to create a simple value unit, and use `computed` to create a derived computation logic:
42
+ Use `state` to store a simple value unit, and use `computed` to create a derived computation logic:
41
43
 
42
44
  ```ts
43
- // atom.js
45
+ // data.js
44
46
  import { state, computed } from 'ccstate';
45
47
 
46
48
  export const userId$ = state('');
@@ -54,14 +56,14 @@ export const user$ = computed(async (get) => {
54
56
  });
55
57
  ```
56
58
 
57
- ### Use Atoms in React
59
+ ### Use data in React
58
60
 
59
- Use `useGet` and `useSet` hooks in React to get/set atoms, and use `useResolved` to get Promise value.
61
+ Use `useGet` and `useSet` hooks in React to get/set data, and use `useResolved` to get Promise value.
60
62
 
61
63
  ```jsx
62
64
  // App.js
63
65
  import { useGet, useSet, useResolved } from 'ccstate';
64
- import { userId$, user$ } from './atom';
66
+ import { userId$, user$ } from './data';
65
67
 
66
68
  export default function App() {
67
69
  const userId = useGet(userId$);
@@ -114,7 +116,7 @@ Through these examples, you should have understood the basic usage of CCState. N
114
116
 
115
117
  ## Core APIs
116
118
 
117
- CCState is an atomic state management library that provides several simple concepts to help developers better manage application states. And it can be used as an external store to drive UI frameworks like React.
119
+ CCState provides several simple concepts to help developers better manage application states. And it can be used as an external store to drive UI frameworks like React.
118
120
 
119
121
  ### State
120
122
 
@@ -130,14 +132,17 @@ store.get(userId$); // 0
130
132
  store.set(userId$, 100);
131
133
  store.get(userId$); // 100
132
134
 
133
- const callback$ = state<(() => void) | undefined>(undefined);
134
- store.set(callback$, () => {
135
- console.log('awesome ccstate');
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'
136
142
  });
137
- store.get(callback$)(); // console log 'awesome ccstate'
138
143
  ```
139
144
 
140
- 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 Atom type from other regular types. Atom types must be accessed through the store's get/set methods, and since it's common to convert an Atom type to a regular type using get, the `$` suffix helps avoid naming conflicts.
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.
141
146
 
142
147
  ### Store
143
148
 
@@ -153,7 +158,7 @@ const otherStore = createStore(); // another new Map()
153
158
  otherStore.get(count$); // anotherMap[$count] ?? $count.init, returns 0
154
159
  ```
155
160
 
156
- This should be easy to understand. If `Store` only needed to support `State` types, a simple Map would be sufficient. However, CCState needs to support two additional atomic types. Next, let's introduce `Computed`, CCState's reactive computation unit.
161
+ This should be easy to understand. If `Store` only needed to support `State` types, a simple Map would be sufficient. However, CCState needs to support two additional data types. Next, let's introduce `Computed`, CCState's reactive computation unit.
157
162
 
158
163
  ### Computed
159
164
 
@@ -184,7 +189,7 @@ In most cases, side-effect free computation logic is extremely useful. They can
184
189
 
185
190
  ### Command
186
191
 
187
- `Command` is CCState's logic unit for organizing side effects. It has both `set` and `get` accessors from the store, allowing it to not only read other Atom values but also modify `State` or call other `Command`.
192
+ `Command` is CCState's logic unit for organizing side effects. It has both `set` and `get` accessors from the store, allowing it to not only read other data types but also modify `State` or call other `Command`.
188
193
 
189
194
  ```typescript
190
195
  import { command, createStore } from 'ccstate';
@@ -279,159 +284,15 @@ That's it! Next, you can learn how to use CCState in React.
279
284
 
280
285
  ## Using in React
281
286
 
282
- To begin using CCState in a React application, you must utilize the `StoreProvider` to provide a store for the hooks.
283
-
284
- ```jsx
285
- // main.tsx
286
- import { createStore, StoreProvider } from 'ccstate';
287
- import { App } from './App';
288
- import { StrictMode } from 'react';
289
- import { createRoot } from 'react-dom/client';
290
-
291
- const store = createStore();
292
-
293
- createRoot(document.getElementById('root')).render(
294
- <StrictMode>
295
- <StoreProvider value={store}>
296
- <App />
297
- </StoreProvider>
298
- </StrictMode>,
299
- );
300
- ```
287
+ [Using in React](docs/react.md)
301
288
 
302
- All descendant components within the `StoreProvider` will use the provided store as the caller for `get` and `set` operations.
289
+ ## Using in Vue
303
290
 
304
- You can place the `StoreProvider` inside or outside of `StrictMode`; the functionality is the same.
291
+ [Using in Vue](docs/vue.md)
305
292
 
306
- ### Retrieving Atom Values
293
+ ### Testing & Debugging
307
294
 
308
- The most basic usage is to use `useGet` to retrieve the value of an Atom.
309
-
310
- ```jsx
311
- // atoms/count.ts
312
- import { state } from 'ccstate';
313
- export const count$ = state(0);
314
-
315
- // App.tsx
316
- import { useGet } from 'ccstate';
317
- import { count$ } from './atoms/count';
318
-
319
- function App() {
320
- const count = useGet(count$);
321
- return <div>{count}</div>;
322
- }
323
- ```
324
-
325
- `useGet` returns a `State` or a `Computed` value, and when the value changes, `useGet` triggers a re-render of the component.
326
-
327
- `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.
328
-
329
- Two other useful hooks are available when dealing with `Promise` values. First, we introduce `useLoadable`.
330
-
331
- ```jsx
332
- // atoms/user.ts
333
- import { computed } from 'ccstate';
334
-
335
- export const user$ = computed(async () => {
336
- return fetch('/api/users/current').then((res) => res.json());
337
- });
338
-
339
- // App.tsx
340
- import { useLoadable } from 'ccstate';
341
- import { user$ } from './atoms/user';
342
-
343
- function App() {
344
- const user_ = useLoadable(user$);
345
- if (user_.state === 'loading') return <div>Loading...</div>;
346
- if (user_.state === 'error') return <div>Error: {user_.error.message}</div>;
347
-
348
- return <div>{user_.data.name}</div>;
349
- }
350
- ```
351
-
352
- `useLoadable` accepts an Atom that returns a `Promise` and wraps the result in a `Loadable` structure.
353
-
354
- ```typescript
355
- type Loadable<T> =
356
- | {
357
- state: 'loading';
358
- }
359
- | {
360
- state: 'hasData';
361
- data: T;
362
- }
363
- | {
364
- state: 'hasError';
365
- error: unknown;
366
- };
367
- ```
368
-
369
- 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`.
370
-
371
- Another useful hook is `useResolved`, which always returns the resolved value of a `Promise`.
372
-
373
- ```jsx
374
- // App.tsx
375
- import { useResolved } from 'ccstate';
376
- import { user$ } from './atoms/user';
377
-
378
- function App() {
379
- const user = useResolved(user$);
380
- return <div>{user?.name}</div>;
381
- }
382
- ```
383
-
384
- `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`.
385
-
386
- ```typescript
387
- // useResolved.ts
388
- import { useLoadable } from './useLoadable';
389
- import type { Computed, State } from '../core';
390
-
391
- export function useResolved<T>(atom: State<Promise<T>> | Computed<Promise<T>>): T | undefined {
392
- const loadable = useLoadable(atom);
393
- return loadable.state === 'hasData' ? loadable.data : undefined;
394
- }
395
- ```
396
-
397
- ### useLastLoadable & useLastResolved
398
-
399
- In some scenarios, we want a refreshable Promise Atom to maintain its previous result during the refresh process instead of showing a loading state. CCState provides `useLastLoadable` and `useLastResolved` to achieve this functionality.
400
-
401
- ```jsx
402
- import { useLoadable } from 'ccstate';
403
- import { user$ } from './atoms/user';
404
-
405
- function App() {
406
- const user_ = useLastLoadable(user$); // Keep the previous result during new user$ request, without triggering loading state
407
- if (user_.state === 'loading') return <div>Loading...</div>;
408
- if (user_.state === 'error') return <div>Error: {user_.error.message}</div>;
409
-
410
- return <div>{user_.data.name}</div>;
411
- }
412
- ```
413
-
414
- `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.
415
-
416
- ### Updating Atom Values / Triggering Funcs
417
-
418
- The `useSet` hook can be used to update the value of an Atom. It returns a function equivalent to `store.set` when called.
419
-
420
- ```jsx
421
- // App.tsx
422
- import { useSet } from 'ccstate';
423
- import { count$ } from './atoms/count';
424
-
425
- function App() {
426
- const setCount = useSet(count$);
427
- // setCount(x => x + 1) is equivalent to store.set(count$, x => x + 1)
428
- return <button onClick={() => setCount((x) => x + 1)}>Increment</button>;
429
- }
430
- ```
431
-
432
- ### Testing & Debugg
433
-
434
- Testing Atoms should be as simple as testing a Map.
295
+ Testing Value/Computed should be as simple as testing a Map.
435
296
 
436
297
  ```typescript
437
298
  // counter.test.ts
@@ -453,22 +314,12 @@ Here are some tips to help you better debug during testing.
453
314
  Use `ConsoleInterceptor` to log most store behaviors to the console during testing:
454
315
 
455
316
  ```typescript
456
- import { ConsoleInterceptor, createDebugStore, state, computed, command } from 'ccstate';
317
+ import { createConsoleDebugStore, state, computed, command } from 'ccstate';
457
318
 
458
319
  const base$ = state(1, { debugLabel: 'base$' });
459
320
  const derived$ = computed((get) => get(base$) * 2);
460
321
 
461
- const interceptor = new ConsoleInterceptor([
462
- {
463
- target: base$,
464
- actions: new Set(['set']), // will only log set actions
465
- },
466
- {
467
- target: derived$, // will log all actions
468
- },
469
- ]);
470
-
471
- const store = createDebugStore(interceptor);
322
+ const store = createConsoleDebugStore([base$, 'derived'], ['set', 'sub']); // log sub & set actions
472
323
  store.set(base$, 1); // console: SET [V0:base$] 1
473
324
  store.sub(
474
325
  derived$,
@@ -478,15 +329,19 @@ store.sub(
478
329
 
479
330
  ## Concept behind CCState
480
331
 
481
- CCState is inspired by Jotai. 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:
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:
482
337
 
483
338
  - Too many combinations of atom init/setter/getter methods, need simplification to reduce team's mental overhead
484
339
  - Should reduce reactive capabilities, especially the `onMount` capability - the framework shouldn't provide this ability
485
340
  - Some implicit magic operations, especially Promise wrapping, make the application execution process less transparent
486
341
 
487
- 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 Atom 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.
488
343
 
489
- ### More Semantic Atom Types
344
+ ### More semantic data types
490
345
 
491
346
  Like Jotai, CCState is also an Atom State solution. However, unlike Jotai, CCState doesn't expose Raw Atom, instead dividing Atoms into three types:
492
347
 
@@ -507,7 +362,7 @@ export const userIdChange$ = command(({ get, set }) => {
507
362
  });
508
363
 
509
364
  // ...
510
- import { userId$, userIdChange$ } from './atoms';
365
+ import { userId$, userIdChange$ } from './data';
511
366
 
512
367
  function setupPage() {
513
368
  const store = createStore();
@@ -519,7 +374,39 @@ function setupPage() {
519
374
 
520
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.
521
376
 
522
- CCState does not have APIs like `onMount`. This is because CCState considers `onMount` to be fundamentally an effect, and providing APIs like `onMount` in `computed` would make the computation process non-idempotent.
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.
523
410
 
524
411
  ### Avoid `useEffect` in React
525
412
 
@@ -556,7 +443,7 @@ export function App() {
556
443
  When designing CCState, we wanted the trigger points for value changes to be completely detached from React's Mount/Unmount lifecycle and completely decoupled from React's rendering behavior.
557
444
 
558
445
  ```jsx
559
- // atoms.js
446
+ // data.js
560
447
  export const userId$ = state(0)
561
448
  export const init$ = command(({set}) => {
562
449
  const userId = // ... parse userId from location search
@@ -587,31 +474,348 @@ root.render(
587
474
  );
588
475
  ```
589
476
 
590
- ## Practices
477
+ ### Less Magic
478
+
479
+ #### No `onMount`: Maintaining Pure State Semantics
591
480
 
592
- ### Naming
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.
593
482
 
594
- Add the suffix `$` to atoms. Since we often need to get values from Atoms in many scenarios, adding the suffix after Atom can avoid naming conflicts.
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):
595
484
 
596
485
  ```typescript
597
- const count$ = state(0);
598
- const double$ = computed((get) => get(count$) * 2);
599
- const updateCount$ = command(({ get, set }, val) => {
600
- set(count$, val);
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
+ };
601
518
  });
602
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
+
603
545
  // ...
604
- const count = get(count$) // will not conflict with normal value
605
546
 
606
- // in react component
607
- const updateCount = useSet(updateCount$) // Command suffix is useful for this
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.
608
776
 
609
- return <button onClick={() => updateCount(10)}>update</button>
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
610
801
  ```
611
802
 
612
- ### Internal Atom
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.
613
817
 
614
- Feel free to create internal Atom. Atom is very lightweight. Creating an Atom should be just like creating a variable. Atoms don't necessarily need to be persisted or defined in the top-level scope - it's perfectly fine to create Atoms inside closures or pass new Atoms through containers.
818
+ So, I think the only way to implement `Computed`'s effect-less is to separate the atom and the accessor.
615
819
 
616
820
  ## Changelog & TODO
617
821