ccstate 2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 e7h4n
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN EFFECT OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,647 @@
1
+ # CCState
2
+
3
+ [![Coverage Status](https://coveralls.io/repos/github/e7h4n/ccstate/badge.svg?branch=main)](https://coveralls.io/github/e7h4n/ccstate?branch=main)
4
+ ![NPM Type Definitions](https://img.shields.io/npm/types/ccstate)
5
+ ![NPM Version](https://img.shields.io/npm/v/ccstate)
6
+ ![npm package minimized gzipped size](https://img.shields.io/bundlejs/size/ccstate)
7
+ [![CI](https://github.com/e7h4n/ccstate/actions/workflows/ci.yaml/badge.svg)](https://github.com/e7h4n/ccstate/actions/workflows/ci.yaml)
8
+ [![CodSpeed Badge](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://codspeed.io/e7h4n/ccstate)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10
+
11
+ CCState is a semantic, strict, and flexible state management library suitable for medium to large single-page applications with complex state management needs.
12
+
13
+ The name CCState comes from its three basic data types: Computed, Command and State.
14
+
15
+ ## Quick Features
16
+
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
22
+
23
+ ## Getting Started
24
+
25
+ ### Installation
26
+
27
+ ```bash
28
+ # npm
29
+ npm i ccstate
30
+
31
+ # pnpm
32
+ pnpm add ccstate
33
+
34
+ # yarn
35
+ yarn add ccstate
36
+ ```
37
+
38
+ ### Create Atoms
39
+
40
+ Use `state` to create a simple value unit, and use `computed` to create a derived computation logic:
41
+
42
+ ```ts
43
+ // atom.js
44
+ import { state, computed } from 'ccstate';
45
+
46
+ export const userId$ = state('');
47
+
48
+ export const user$ = computed(async (get) => {
49
+ const userId = get(userId$);
50
+ if (!userId) return null;
51
+
52
+ const resp = await fetch(`https://api.github.com/users/${userId}`);
53
+ return resp.json();
54
+ });
55
+ ```
56
+
57
+ ### Use Atoms in React
58
+
59
+ Use `useGet` and `useSet` hooks in React to get/set atoms, and use `useResolved` to get Promise value.
60
+
61
+ ```jsx
62
+ // App.js
63
+ import { useGet, useSet, useResolved } from 'ccstate';
64
+ import { userId$, user$ } from './atom';
65
+
66
+ export default function App() {
67
+ const userId = useGet(userId$);
68
+ const setUserId = useSet(userId$);
69
+ const user = useResolved(user$);
70
+
71
+ return (
72
+ <div>
73
+ <div>
74
+ <input type="text" value={userId} onChange={(e) => setUserId(e.target.value)} placeholder="github username" />
75
+ </div>
76
+ <div>
77
+ <img src={user?.avatar_url} width="48" />
78
+ <div>
79
+ {user?.name}
80
+ {user?.company}
81
+ </div>
82
+ </div>
83
+ </div>
84
+ );
85
+ }
86
+ ```
87
+
88
+ Use `createStore` and `StoreProvider` to provide a CCState store to React, all states and computations will only affect this isolated store.
89
+
90
+ ```tsx
91
+ // main.jsx
92
+ import { createStore, StoreProvider } from 'ccstate';
93
+ import { StrictMode } from 'react';
94
+ import { createRoot } from 'react-dom/client';
95
+
96
+ import App from './App';
97
+
98
+ const rootElement = document.getElementById('root');
99
+ const root = createRoot(rootElement);
100
+
101
+ const store = createStore();
102
+ root.render(
103
+ <StrictMode>
104
+ <StoreProvider value={store}>
105
+ <App />
106
+ </StoreProvider>
107
+ </StrictMode>,
108
+ );
109
+ ```
110
+
111
+ That's it! [Click here to see the full example](https://codesandbox.io/p/sandbox/cr3xg6).
112
+
113
+ Through these examples, you should have understood the basic usage of CCState. Next, you can read to learn about CCState's core APIs.
114
+
115
+ ## Core APIs
116
+
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.
118
+
119
+ ### State
120
+
121
+ `State` is the most basic value unit in CCState. A `State` can store any type of value, which can be accessed or modified through the store's `get`/`set` methods. Before explaining why it's designed this way, let's first look at the basic capabilities of `State`.
122
+
123
+ ```typescript
124
+ import { store, state } from 'ccstate';
125
+
126
+ const store = createStore();
127
+
128
+ const userId$ = state(0);
129
+ store.get(userId$); // 0
130
+ store.set(userId$, 100);
131
+ store.get(userId$); // 100
132
+
133
+ const callback$ = state<(() => void) | undefined>(undefined);
134
+ store.set(callback$, () => {
135
+ console.log('awesome ccstate');
136
+ });
137
+ store.get(callback$)(); // console log 'awesome ccstate'
138
+ ```
139
+
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.
141
+
142
+ ### Store
143
+
144
+ In CCState, declaring a `State` doesn't mean the value will be stored within the `State` itself. In fact, a `State` acts like a key in a Map, and CCState needs to create a Map to store the corresponding value for each `State` - this Map is the `Store`.
145
+
146
+ ```typescript
147
+ const count$ = state(0); // count$: { init: 0 }
148
+
149
+ const store = createStore(); // imagine this as new Map()
150
+ store.set(count$, 10); // simply imagine as map[count$] = 10
151
+
152
+ const otherStore = createStore(); // another new Map()
153
+ otherStore.get(count$); // anotherMap[$count] ?? $count.init, returns 0
154
+ ```
155
+
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.
157
+
158
+ ### Computed
159
+
160
+ `Computed` is CCState's reactive computation unit. You can write derived computation logic in `Computed`, such as sending HTTP requests, data transformation, data aggregation, etc.
161
+
162
+ ```typescript
163
+ import { computed, createStore } from 'ccstate';
164
+
165
+ const userId$ = state(0);
166
+ const user$ = computed(async (get) => {
167
+ const userId = get(userId$);
168
+ const resp = await fetch('/api/users/' + userId);
169
+ return resp.json();
170
+ });
171
+
172
+ const store = createStore();
173
+ const user = await store.get(user$);
174
+ ```
175
+
176
+ Does this example seem less intuitive than `State`? Here's a mental model that might help you better understand what's happening:
177
+
178
+ - `computed(fn)` returns an object `{read: fn}`, which is assigned to `user$`
179
+ - When `store.get(user$)` encounters an object which has a read function, it calls that function: `user$.read(store.get)`
180
+
181
+ This way, `Computed` receives a get accessor that can access other data in the store. This get accessor is similar to `store.get` and can be used to read both `State` and `Computed`. The reason CCState specifically passes a get method to `Computed`, rather than allowing direct access to the store within `Computed`, is to shield the logic within `Computed` from other store methods like `store.set`. The key characteristic of `Computed` is that it can only read states from the store but cannot modify them. In other words, `Computed` is side-effect free.
182
+
183
+ In most cases, side-effect free computation logic is extremely useful. They can be executed any number of times and have few requirements regarding execution timing. `Computed` is one of the most powerful features in CCState, and you should try to write your logic as `Computed` whenever possible, unless you need to perform set operations on the `Store`.
184
+
185
+ ### Command
186
+
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`.
188
+
189
+ ```typescript
190
+ import { command, createStore } from 'ccstate';
191
+
192
+ const user$ = state<UserInfo | undefined>(undefined);
193
+ const updateUser$ = command(async ({ set }, userId) => {
194
+ const user = await fetch('/api/users/' + userId).then((resp) => resp.json());
195
+ set(user$, user);
196
+ });
197
+
198
+ const store = createStore();
199
+ store.set(updateUser$, 10); // fetchUserInfo(userId=10) and set to user$
200
+ ```
201
+
202
+ Similarly, we can imagine the set operation like this:
203
+
204
+ - `command(fn)` returns an object `{write: fn}` which is assigned to `updateUser$`
205
+ - When `store.set(updateUser$)` encounters an object which has a `write` function, it calls that function: `updateUser$.write({set: store.set, get: store.get}, userId)`
206
+
207
+ Since `Command` can call the `set` method, it produces side effects on the `Store`. Therefore, its execution timing must be explicitly specified through one of these ways:
208
+
209
+ - Calling a `Command` through `store.set`
210
+ - Being called by the `set` method within other `Command`s
211
+ - Being triggered by subscription relationships established through `store.sub`
212
+
213
+ ### Subscribing to Changes
214
+
215
+ CCState provides a `sub` method on the store to establish subscription relationships.
216
+
217
+ ```typescript
218
+ import { createStore, state, computed, command } from 'ccstate';
219
+
220
+ const base$ = state(0);
221
+ const double$ = computed((get) => get(base$) * 2);
222
+
223
+ const store = createStore();
224
+ store.sub(
225
+ double$,
226
+ command(({ get }) => {
227
+ console.log('double', get(double$));
228
+ }),
229
+ );
230
+
231
+ store.set(base$, 10); // will log to console 'double 20'
232
+ ```
233
+
234
+ There are two ways to unsubscribe:
235
+
236
+ 1. Using the `unsub` function returned by `store.sub`
237
+ 2. Using an AbortSignal to control the subscription
238
+
239
+ The `sub` method is powerful but should be used carefully. In most cases, `Computed` is a better choice than `sub` because `Computed` doesn't generate new `set` operations.
240
+
241
+ ```typescript
242
+ // 🙅 use sub
243
+ const user$ = state(undefined);
244
+ const userId$ = state(0);
245
+ store.sub(
246
+ userId$,
247
+ command(({ set, get }) => {
248
+ const userId = get(userId$);
249
+ const user = fetch('/api/users/' + userId).then((resp) => resp.json());
250
+ set(user$, user);
251
+ }),
252
+ );
253
+
254
+ // ✅ use computed
255
+ const userId$ = state(0);
256
+ const user$ = computed(async (get) => {
257
+ return await fetch('/api/users/' + get(userId$)).then((resp) => resp.json());
258
+ });
259
+ ```
260
+
261
+ Using `Computed` to write reactive logic has several advantages:
262
+
263
+ - No need to manage unsubscription
264
+ - No need to worry about it modifying other `State`s or calling other `Command`
265
+
266
+ Here's a simple rule of thumb:
267
+
268
+ > if some logic can be written as a `Computed`, it should be written as a `Computed`.
269
+
270
+ ### Comprasion
271
+
272
+ | Type | get | set | sub target | as sub callback |
273
+ | -------- | --- | --- | ---------- | --------------- |
274
+ | State | ✅ | ✅ | ✅ | ❌ |
275
+ | Computed | ✅ | ❌ | ✅ | ❌ |
276
+ | Command | ❌ | ✅ | ❌ | ✅ |
277
+
278
+ That's it! Next, you can learn how to use CCState in React.
279
+
280
+ ## Using in React
281
+
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
+ ```
301
+
302
+ All descendant components within the `StoreProvider` will use the provided store as the caller for `get` and `set` operations.
303
+
304
+ You can place the `StoreProvider` inside or outside of `StrictMode`; the functionality is the same.
305
+
306
+ ### Retrieving Atom Values
307
+
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.
435
+
436
+ ```typescript
437
+ // counter.test.ts
438
+ import { test } from 'vitest';
439
+ import { createStore, state } from 'ccstate';
440
+
441
+ test('test counter', () => {
442
+ const store = createStore();
443
+ const count$ = state(0);
444
+ store.set(count$, 10);
445
+ expect(store.get(count$)).toBe(10);
446
+ });
447
+ ```
448
+
449
+ Here are some tips to help you better debug during testing.
450
+
451
+ ### ConsoleInterceptor
452
+
453
+ Use `ConsoleInterceptor` to log most store behaviors to the console during testing:
454
+
455
+ ```typescript
456
+ import { ConsoleInterceptor, createDebugStore, state, computed, command } from 'ccstate';
457
+
458
+ const base$ = state(1, { debugLabel: 'base$' });
459
+ const derived$ = computed((get) => get(base$) * 2);
460
+
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);
472
+ store.set(base$, 1); // console: SET [V0:base$] 1
473
+ store.sub(
474
+ derived$,
475
+ command(() => void 0),
476
+ ); // console: SUB [V0:derived$]
477
+ ```
478
+
479
+ ## Concept behind CCState
480
+
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:
482
+
483
+ - Too many combinations of atom init/setter/getter methods, need simplification to reduce team's mental overhead
484
+ - Should reduce reactive capabilities, especially the `onMount` capability - the framework shouldn't provide this ability
485
+ - Some implicit magic operations, especially Promise wrapping, make the application execution process less transparent
486
+
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.
488
+
489
+ ### More Semantic Atom Types
490
+
491
+ 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
+
493
+ - `State` (equivalent to "Primitive Atom" in Jotai): `State` is a readable and writable "variable", similar to a Primitive Atom in Jotai. Reading a `State` involves no computation process, and writing to a `State` just like a map.set.
494
+ - `Computed` (equivalent to "Read-only Atom" in Jotai): `Computed` is a readable computed variable whose calculation process should be side-effect free. As long as its dependent Atoms don't change, repeatedly reading the value of a `Computed` should yield identical results. `Computed` is similar to a Read-only Atom in Jotai.
495
+ - `Command` (equivalent to "Write-only Atom" in Jotai): `Command` is used to encapsulate a process code block. The code inside an Command only executes when an external `set` call is made on it. `Command` is also the only type in ccstate that can modify value without relying on a store.
496
+
497
+ ### Subscription System
498
+
499
+ CCState's subscription system is different from Jotai's. First, CCState's subscription callback must be an `Command`.
500
+
501
+ ```typescript
502
+ export const userId$ = state(1);
503
+
504
+ export const userIdChange$ = command(({ get, set }) => {
505
+ const userId = get(userId$);
506
+ // ...
507
+ });
508
+
509
+ // ...
510
+ import { userId$, userIdChange$ } from './atoms';
511
+
512
+ function setupPage() {
513
+ const store = createStore();
514
+ // ...
515
+ store.sub(userId$, userIdChange$);
516
+ // ...
517
+ }
518
+ ```
519
+
520
+ 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
+
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.
523
+
524
+ ### Avoid `useEffect` in React
525
+
526
+ While Reactive Programming like `useEffect` has natural advantages in decoupling View Components, it causes many complications for editor applications like [Motiff](https://motiff.com).
527
+
528
+ Regardless of the original design semantics of `useEffect`, in the current environment, `useEffect`'s semantics are deeply bound to React's rendering behavior. When engineers use `useEffect`, they subconsciously think "callback me when these things change", especially "callback me when some async process is done". While it's easy to write such waiting code using `async/await`, it feels unnatural in React.
529
+
530
+ ```jsx
531
+ // App.jsx
532
+ // Reactive Programming in React
533
+ export function App() {
534
+ const userId = useUserId(); // an common hook to takeout userId from current location search params
535
+ const [user, setUser] = useState();
536
+ const [loading, setLoading] = useState();
537
+
538
+ useEffect(() => {
539
+ setLoading(true);
540
+ fetch('/api/users/' + userId)
541
+ .then((resp) => resp.json())
542
+ .then((u) => {
543
+ setLoading(false);
544
+ setUser(u);
545
+ });
546
+ }, [userId]);
547
+
548
+ if (loading) {
549
+ return <div>Loading...</div>;
550
+ }
551
+
552
+ return <>{user?.name}</>;
553
+ }
554
+ ```
555
+
556
+ 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
+
558
+ ```jsx
559
+ // atoms.js
560
+ export const userId$ = state(0)
561
+ export const init$ = command(({set}) => {
562
+ const userId = // ... parse userId from location search
563
+ set(userId$, userId)
564
+ })
565
+
566
+ export const user$ = computed(get => {
567
+ const userId = get(userId$)
568
+ return fetch('/api/users/' + userId).then(resp => resp.json())
569
+ })
570
+
571
+ // App.jsx
572
+ export function App() {
573
+ const user = useLastResolved(user$);
574
+ return <>{user?.name}</>;
575
+ }
576
+
577
+ // main.jsx
578
+ const store = createStore();
579
+ store.set(init$)
580
+
581
+ const rootElement = document.getElementById('root')!;
582
+ const root = createRoot(rootElement);
583
+ root.render(
584
+ <StoreProvider value={store}>
585
+ <App />
586
+ </StoreProvider>,
587
+ );
588
+ ```
589
+
590
+ ## Practices
591
+
592
+ ### Naming
593
+
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.
595
+
596
+ ```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);
601
+ });
602
+
603
+ // ...
604
+ const count = get(count$) // will not conflict with normal value
605
+
606
+ // in react component
607
+ const updateCount = useSet(updateCount$) // Command suffix is useful for this
608
+
609
+ return <button onClick={() => updateCount(10)}>update</button>
610
+ ```
611
+
612
+ ### Internal Atom
613
+
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.
615
+
616
+ ## Changelog & TODO
617
+
618
+ [Changelog](packages/ccstate/CHANGELOG.md)
619
+
620
+ Here are some new ideas:
621
+
622
+ - Integration with svelte / solid.js
623
+ - Enhance devtools
624
+ - Support viewing current subscription graph and related atom values
625
+ - Enable logging and breakpoints for specific atoms in devtools
626
+ - Performance improvements
627
+ - Mount atomState directly on atoms when there's only one store in the application to reduce WeakMap lookup overhead
628
+ - Support static declaration of upstream dependencies for Computed to improve performance by disabling runtime dependency analysis
629
+
630
+ ## Contributing
631
+
632
+ CCState welcomes any suggestions and Pull Requests. If you're interested in improving CCState, here are some basic steps to help you set up a CCState development environment.
633
+
634
+ ```bash
635
+ pnpm install
636
+ pnpm husky # setup commit hooks to verify commit
637
+ pnpm vitest # to run all tests
638
+ pnpm lint # check code style & typing
639
+ ```
640
+
641
+ ## Special Thanks
642
+
643
+ Thanks [Jotai](https://github.com/pmndrs/jotai) for the inspiration and some code snippets, especially the test cases. Without their work, this project would not exist.
644
+
645
+ ## License
646
+
647
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.