atomirx 0.0.8 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/README.md +198 -2234
  2. package/bin/cli.js +90 -0
  3. package/dist/core/derived.d.ts +2 -2
  4. package/dist/core/effect.d.ts +3 -2
  5. package/dist/core/onCreateHook.d.ts +15 -2
  6. package/dist/core/onErrorHook.d.ts +4 -1
  7. package/dist/core/pool.d.ts +78 -0
  8. package/dist/core/pool.test.d.ts +1 -0
  9. package/dist/core/select-boolean.test.d.ts +1 -0
  10. package/dist/core/select-pool.test.d.ts +1 -0
  11. package/dist/core/select.d.ts +278 -86
  12. package/dist/core/types.d.ts +233 -1
  13. package/dist/core/withAbort.d.ts +95 -0
  14. package/dist/core/withReady.d.ts +3 -3
  15. package/dist/devtools/constants.d.ts +41 -0
  16. package/dist/devtools/index.cjs +1 -0
  17. package/dist/devtools/index.d.ts +29 -0
  18. package/dist/devtools/index.js +429 -0
  19. package/dist/devtools/registry.d.ts +98 -0
  20. package/dist/devtools/registry.test.d.ts +1 -0
  21. package/dist/devtools/setup.d.ts +61 -0
  22. package/dist/devtools/types.d.ts +311 -0
  23. package/dist/index-BZEnfIcB.cjs +1 -0
  24. package/dist/index-BbPZhsDl.js +1653 -0
  25. package/dist/index.cjs +1 -1
  26. package/dist/index.d.ts +4 -3
  27. package/dist/index.js +18 -14
  28. package/dist/onDispatchHook-C8yLzr-o.cjs +1 -0
  29. package/dist/onDispatchHook-SKbiIUaJ.js +5 -0
  30. package/dist/onErrorHook-BGGy3tqK.js +38 -0
  31. package/dist/onErrorHook-DHBASmYw.cjs +1 -0
  32. package/dist/react/index.cjs +1 -1
  33. package/dist/react/index.js +191 -151
  34. package/dist/react/onDispatchHook.d.ts +106 -0
  35. package/dist/react/useAction.d.ts +4 -1
  36. package/dist/react-devtools/DevToolsPanel.d.ts +93 -0
  37. package/dist/react-devtools/EntityDetails.d.ts +10 -0
  38. package/dist/react-devtools/EntityList.d.ts +15 -0
  39. package/dist/react-devtools/LogList.d.ts +12 -0
  40. package/dist/react-devtools/hooks.d.ts +50 -0
  41. package/dist/react-devtools/index.cjs +1 -0
  42. package/dist/react-devtools/index.d.ts +31 -0
  43. package/dist/react-devtools/index.js +1589 -0
  44. package/dist/react-devtools/styles.d.ts +148 -0
  45. package/package.json +26 -2
  46. package/skills/atomirx/SKILL.md +456 -0
  47. package/skills/atomirx/references/async-patterns.md +188 -0
  48. package/skills/atomirx/references/atom-patterns.md +238 -0
  49. package/skills/atomirx/references/deferred-loading.md +191 -0
  50. package/skills/atomirx/references/derived-patterns.md +428 -0
  51. package/skills/atomirx/references/effect-patterns.md +426 -0
  52. package/skills/atomirx/references/error-handling.md +140 -0
  53. package/skills/atomirx/references/hooks.md +322 -0
  54. package/skills/atomirx/references/pool-patterns.md +229 -0
  55. package/skills/atomirx/references/react-integration.md +411 -0
  56. package/skills/atomirx/references/rules.md +407 -0
  57. package/skills/atomirx/references/select-context.md +309 -0
  58. package/skills/atomirx/references/service-template.md +172 -0
  59. package/skills/atomirx/references/store-template.md +205 -0
  60. package/skills/atomirx/references/testing-patterns.md +431 -0
  61. package/coverage/base.css +0 -224
  62. package/coverage/block-navigation.js +0 -87
  63. package/coverage/clover.xml +0 -1440
  64. package/coverage/coverage-final.json +0 -14
  65. package/coverage/favicon.png +0 -0
  66. package/coverage/index.html +0 -131
  67. package/coverage/prettify.css +0 -1
  68. package/coverage/prettify.js +0 -2
  69. package/coverage/sort-arrow-sprite.png +0 -0
  70. package/coverage/sorter.js +0 -210
  71. package/coverage/src/core/atom.ts.html +0 -889
  72. package/coverage/src/core/batch.ts.html +0 -223
  73. package/coverage/src/core/define.ts.html +0 -805
  74. package/coverage/src/core/emitter.ts.html +0 -919
  75. package/coverage/src/core/equality.ts.html +0 -631
  76. package/coverage/src/core/hook.ts.html +0 -460
  77. package/coverage/src/core/index.html +0 -281
  78. package/coverage/src/core/isAtom.ts.html +0 -100
  79. package/coverage/src/core/isPromiseLike.ts.html +0 -133
  80. package/coverage/src/core/onCreateHook.ts.html +0 -138
  81. package/coverage/src/core/scheduleNotifyHook.ts.html +0 -94
  82. package/coverage/src/core/types.ts.html +0 -523
  83. package/coverage/src/core/withUse.ts.html +0 -253
  84. package/coverage/src/index.html +0 -116
  85. package/coverage/src/index.ts.html +0 -106
  86. package/dist/index-CBVj1kSj.js +0 -1350
  87. package/dist/index-Cxk9v0um.cjs +0 -1
  88. package/scripts/publish.js +0 -198
  89. package/src/core/atom.test.ts +0 -633
  90. package/src/core/atom.ts +0 -311
  91. package/src/core/atomState.test.ts +0 -342
  92. package/src/core/atomState.ts +0 -256
  93. package/src/core/batch.test.ts +0 -257
  94. package/src/core/batch.ts +0 -172
  95. package/src/core/define.test.ts +0 -343
  96. package/src/core/define.ts +0 -243
  97. package/src/core/derived.test.ts +0 -1215
  98. package/src/core/derived.ts +0 -450
  99. package/src/core/effect.test.ts +0 -802
  100. package/src/core/effect.ts +0 -188
  101. package/src/core/emitter.test.ts +0 -364
  102. package/src/core/emitter.ts +0 -392
  103. package/src/core/equality.test.ts +0 -392
  104. package/src/core/equality.ts +0 -182
  105. package/src/core/getAtomState.ts +0 -69
  106. package/src/core/hook.test.ts +0 -227
  107. package/src/core/hook.ts +0 -177
  108. package/src/core/isAtom.ts +0 -27
  109. package/src/core/isPromiseLike.test.ts +0 -72
  110. package/src/core/isPromiseLike.ts +0 -16
  111. package/src/core/onCreateHook.ts +0 -107
  112. package/src/core/onErrorHook.test.ts +0 -350
  113. package/src/core/onErrorHook.ts +0 -52
  114. package/src/core/promiseCache.test.ts +0 -241
  115. package/src/core/promiseCache.ts +0 -284
  116. package/src/core/scheduleNotifyHook.ts +0 -53
  117. package/src/core/select.ts +0 -729
  118. package/src/core/selector.test.ts +0 -799
  119. package/src/core/types.ts +0 -389
  120. package/src/core/withReady.test.ts +0 -534
  121. package/src/core/withReady.ts +0 -191
  122. package/src/core/withUse.test.ts +0 -249
  123. package/src/core/withUse.ts +0 -56
  124. package/src/index.test.ts +0 -80
  125. package/src/index.ts +0 -65
  126. package/src/react/index.ts +0 -21
  127. package/src/react/rx.test.tsx +0 -571
  128. package/src/react/rx.tsx +0 -531
  129. package/src/react/strictModeTest.tsx +0 -71
  130. package/src/react/useAction.test.ts +0 -987
  131. package/src/react/useAction.ts +0 -607
  132. package/src/react/useSelector.test.ts +0 -182
  133. package/src/react/useSelector.ts +0 -292
  134. package/src/react/useStable.test.ts +0 -553
  135. package/src/react/useStable.ts +0 -288
  136. package/tsconfig.json +0 -9
  137. package/v2.md +0 -725
  138. package/vite.config.ts +0 -42
@@ -0,0 +1,407 @@
1
+ # Rules & Best Practices
2
+
3
+ ## Service vs Store (CRITICAL)
4
+
5
+ **All state/logic MUST use `define()`. Services = stateless. Stores = atoms.**
6
+
7
+ | Type | Purpose | Variable | File | Contains |
8
+ | ----------- | -------------- | ------------- | ----------------- | ----------------------- |
9
+ | **Service** | Stateless I/O | `authService` | `auth.service.ts` | Pure functions |
10
+ | **Store** | Reactive state | `authStore` | `auth.store.ts` | Atoms, derived, effects |
11
+
12
+ ### Service
13
+
14
+ ```typescript
15
+ export const authService = define(
16
+ (): AuthService => ({
17
+ checkSupport: async () => {
18
+ /* WebAuthn API */
19
+ },
20
+ register: async (opts) => {
21
+ /* credential creation */
22
+ },
23
+ authenticate: async (opts) => {
24
+ /* credential assertion */
25
+ },
26
+ })
27
+ );
28
+ ```
29
+
30
+ ### Store
31
+
32
+ ```typescript
33
+ import { authService } from "@/services/auth/auth.service";
34
+
35
+ export const authStore = define(() => {
36
+ const auth = authService(); // Inject via invocation
37
+
38
+ const user$ = atom<User | null>(null, { meta: { key: "auth.user" } });
39
+ const isAuthenticated$ = derived(({ read }) => read(user$) !== null, {
40
+ meta: { key: "auth.isAuthenticated" },
41
+ });
42
+
43
+ return {
44
+ ...readonly({ user$, isAuthenticated$ }),
45
+ login: async () => {
46
+ const result = await auth.authenticate({});
47
+ if (result.success) user$.set(result.user);
48
+ },
49
+ };
50
+ });
51
+ ```
52
+
53
+ ### FORBIDDEN: Factory Pattern
54
+
55
+ ```typescript
56
+ // ❌ FORBIDDEN
57
+ let instance: AuthService | null = null;
58
+ export function getAuthService(): AuthService {
59
+ if (!instance) instance = createAuthService();
60
+ return instance;
61
+ }
62
+
63
+ import { getAuthService } from "@/services/auth";
64
+ const auth = getAuthService(); // WRONG
65
+
66
+ // ✅ REQUIRED
67
+ import { authService } from "@/services/auth/auth.service";
68
+ const auth = authService(); // Module invocation
69
+ ```
70
+
71
+ **Detection:** `get*Service()`, `create*Service()`, `*Factory()` → STOP, refactor to `define()`.
72
+
73
+ | Factory Pattern | Module Pattern (`define()`) |
74
+ | --------------- | --------------------------- |
75
+ | Not mockable | `service.override(mock)` |
76
+ | Hidden deps | Explicit dependencies |
77
+ | No lazy control | Lazy singleton default |
78
+ | Breaks DI | Uses atomirx DI |
79
+
80
+ ## useSelector Grouping (CRITICAL)
81
+
82
+ **MUST group multiple reads into single `useSelector`.**
83
+
84
+ ```tsx
85
+ // ✅ DO
86
+ const { count, user, settings } = useSelector(({ read }) => ({
87
+ count: read(count$),
88
+ user: read(user$),
89
+ settings: read(settings$),
90
+ }));
91
+
92
+ // ❌ DON'T
93
+ const count = useSelector(count$);
94
+ const user = useSelector(user$);
95
+ const settings = useSelector(settings$);
96
+ ```
97
+
98
+ | Multiple Calls | Single Grouped |
99
+ | ------------------- | ---------------- |
100
+ | N subscriptions | 1 subscription |
101
+ | N checks per change | 1 check |
102
+ | Scattered values | Related together |
103
+
104
+ **Single `useSelector(atom$)` OK when:** only one atom needed.
105
+
106
+ ## useAction with Atom Deps
107
+
108
+ **Pass atoms to `deps`, use `.get()` inside for auto re-dispatch.**
109
+
110
+ ```tsx
111
+ // ✅ DO
112
+ const load = useAction(
113
+ async () => {
114
+ const val1 = atom1$.get();
115
+ const val2 = await atom2$.get();
116
+ return val1 + val2;
117
+ },
118
+ { deps: [atom1$, atom2$], lazy: false }
119
+ );
120
+
121
+ // ❌ DON'T
122
+ const { val1, val2 } = useSelector(({ read }) => ({
123
+ val1: read(atom1$), // Suspends before useAction
124
+ val2: read(atom2$),
125
+ }));
126
+ const load = useAction(async () => val1 + val2, {
127
+ deps: [val1, val2],
128
+ lazy: false,
129
+ });
130
+ ```
131
+
132
+ ## define() Isolation (CRITICAL)
133
+
134
+ **MUST use `define()` for all state/logic.**
135
+
136
+ ```typescript
137
+ // ✅ DO
138
+ export const counterStore = define(() => {
139
+ const storage = storageService();
140
+ const count$ = atom(0, { meta: { key: "counter.count" } });
141
+
142
+ return {
143
+ ...readonly({ count$ }),
144
+ increment: () => count$.set((x) => x + 1),
145
+ save: () => storage.set("count", count$.get()),
146
+ };
147
+ });
148
+
149
+ // ❌ DON'T
150
+ const count$ = atom(0); // Global, not testable
151
+ ```
152
+
153
+ | Benefit | Description |
154
+ | --------------- | ------------------------------- |
155
+ | Testing/Mocking | Override for unit tests |
156
+ | Lazy init | Only when first accessed |
157
+ | DI | Depend on services/stores |
158
+ | Environment | Override per platform |
159
+ | Encapsulation | `readonly()` prevents mutations |
160
+
161
+ ### Override Pattern
162
+
163
+ ```typescript
164
+ const storageService = define((): StorageService => {
165
+ throw new Error("Not implemented");
166
+ });
167
+
168
+ // Platform implementations
169
+ const webStorage = define(
170
+ (): StorageService => ({
171
+ get: (key) => localStorage.getItem(key),
172
+ set: (key, val) => localStorage.setItem(key, val),
173
+ })
174
+ );
175
+
176
+ // Override based on environment
177
+ if (isWeb) storageService.override(webStorage);
178
+
179
+ // In tests
180
+ storageService.override(() => ({
181
+ get: jest.fn(),
182
+ set: jest.fn(),
183
+ }));
184
+ ```
185
+
186
+ ## batch() for Multiple Updates
187
+
188
+ **MUST wrap multiple updates in `batch()`.**
189
+
190
+ ```typescript
191
+ // ✅ DO
192
+ batch(() => {
193
+ user$.set(newUser);
194
+ settings$.set(newSettings);
195
+ lastUpdated$.set(Date.now());
196
+ }); // Single notification
197
+
198
+ // ❌ DON'T
199
+ user$.set(newUser); // Notification 1
200
+ settings$.set(newSettings); // Notification 2
201
+ lastUpdated$.set(Date.now()); // Notification 3
202
+ ```
203
+
204
+ | Without batch | With batch |
205
+ | ------------------- | ---------------- |
206
+ | N notifications | 1 notification |
207
+ | Intermediate states | Only final state |
208
+ | UI flicker | Clean update |
209
+
210
+ ## Single Effect, Single Workflow (CRITICAL)
211
+
212
+ **Each effect = ONE workflow. Split multiple workflows.**
213
+
214
+ ```typescript
215
+ // ❌ WRONG
216
+ effect(({ read }) => {
217
+ const id = read(currentId$);
218
+ const filter = read(filter$);
219
+ fetchEntity(id); // Workflow 1
220
+ localStorage.setItem("filter", filter); // Workflow 2
221
+ trackPageView(id); // Workflow 3
222
+ });
223
+
224
+ // ✅ CORRECT
225
+ effect(
226
+ ({ read }) => {
227
+ const id = read(currentId$);
228
+ if (id) fetchEntity(id);
229
+ },
230
+ { meta: { key: "fetch.entity" } }
231
+ );
232
+
233
+ effect(
234
+ ({ read }) => {
235
+ localStorage.setItem("filter", read(filter$));
236
+ },
237
+ { meta: { key: "persist.filter" } }
238
+ );
239
+
240
+ effect(
241
+ ({ read }) => {
242
+ const id = read(currentId$);
243
+ if (id) trackPageView(id);
244
+ },
245
+ { meta: { key: "analytics.pageView" } }
246
+ );
247
+ ```
248
+
249
+ | Multiple Workflows | Single Workflow |
250
+ | ------------------ | -------------------- |
251
+ | Hard to trace | Clear cause → effect |
252
+ | Combined triggers | Independent |
253
+ | Hard to test | Test in isolation |
254
+ | Hard to disable | Comment one effect |
255
+
256
+ ## meta.key (CRITICAL)
257
+
258
+ **MUST define for ALL atoms, derived, effects.**
259
+
260
+ ```typescript
261
+ // ✅ CORRECT
262
+ const user$ = atom<User | null>(null, { meta: { key: "auth.user" } });
263
+ const isAuth$ = derived(({ read }) => read(user$) !== null, {
264
+ meta: { key: "auth.isAuthenticated" },
265
+ });
266
+ effect(({ read }) => analytics.identify(read(user$)?.id), {
267
+ meta: { key: "auth.identifyUser" },
268
+ });
269
+
270
+ // ❌ WRONG
271
+ const user$ = atom<User | null>(null);
272
+ const isAuth$ = derived(({ read }) => read(user$) !== null);
273
+ ```
274
+
275
+ | Pattern | Example |
276
+ | ------------------- | ---------------------- |
277
+ | `store.atomName` | `auth.user` |
278
+ | `store.derivedName` | `auth.isAuthenticated` |
279
+ | `store.effectName` | `sync.autoSave` |
280
+
281
+ ## Atom Storage
282
+
283
+ **NEVER store atoms in component scope.**
284
+
285
+ ```typescript
286
+ // ❌ BAD - memory leak
287
+ function Component() {
288
+ const data$ = useRef(atom(0)).current;
289
+ }
290
+
291
+ // ✅ GOOD
292
+ const dataStore = define(() => {
293
+ const data$ = atom(0, { meta: { key: "data" } });
294
+ return { ...readonly({ data$ }), update: (v) => data$.set(v) };
295
+ });
296
+ ```
297
+
298
+ ## Mutation Co-location
299
+
300
+ **All mutations MUST be in the store that owns the atom.**
301
+
302
+ ```typescript
303
+ // ✅ CORRECT
304
+ const counterStore = define(() => {
305
+ const count$ = atom(0, { meta: { key: "counter.count" } });
306
+ return {
307
+ ...readonly({ count$ }),
308
+ increment: () => count$.set((p) => p + 1),
309
+ decrement: () => count$.set((p) => p - 1),
310
+ reset: () => count$.reset(),
311
+ };
312
+ });
313
+
314
+ // ❌ WRONG
315
+ const { count$ } = counterStore();
316
+ count$.set(10); // External mutation
317
+ ```
318
+
319
+ ## SelectContext: Sync Only
320
+
321
+ **All context methods MUST be called synchronously.**
322
+
323
+ ```typescript
324
+ // ❌ WRONG
325
+ derived(({ read }) => {
326
+ setTimeout(() => read(atom$), 100); // Error
327
+ return "value";
328
+ });
329
+
330
+ // ✅ CORRECT
331
+ effect(({ read }) => {
332
+ const config = read(config$);
333
+ setTimeout(() => {
334
+ const data = myAtom$.get(); // Use .get() for async
335
+ console.log(data);
336
+ }, 100);
337
+ });
338
+ ```
339
+
340
+ ## Error Handling: safe() Not try/catch
341
+
342
+ **NEVER try/catch with read() — breaks Suspense.**
343
+
344
+ ```typescript
345
+ // ❌ WRONG
346
+ derived(({ read }) => {
347
+ try {
348
+ return read(asyncAtom$);
349
+ } catch (e) {
350
+ return null; // Catches Promise!
351
+ }
352
+ });
353
+
354
+ // ✅ CORRECT
355
+ derived(({ read, safe }) => {
356
+ const [err, value] = safe(() => read(asyncAtom$));
357
+ if (err) return { error: err.message };
358
+ return { value };
359
+ });
360
+ ```
361
+
362
+ ## Naming Conventions
363
+
364
+ ```typescript
365
+ // All atoms: $ suffix
366
+ const count$ = atom(0);
367
+ const user$ = atom<User | null>(null);
368
+ const productList$ = atom(fetchProducts()); // Async — still just $
369
+ const config$ = atom(loadConfig());
370
+
371
+ // Derived: $ suffix
372
+ const doubled$ = derived(({ read }) => read(count$) * 2);
373
+ const userName$ = derived(({ read }) => read(user$).name);
374
+
375
+ // Services: *Service (NO atoms)
376
+ const authService = define((): AuthService => ...);
377
+
378
+ // Stores: *Store (HAS atoms)
379
+ const authStore = define(() => ...);
380
+
381
+ // Actions: verb-led
382
+ navigateTo, invalidate, refresh, fetchUser, logout
383
+ ```
384
+
385
+ | Type | Suffix | Example |
386
+ | -------------------- | --------- | ------------------------ |
387
+ | Atom (sync or async) | `$` | `count$`, `productList$` |
388
+ | Derived | `$` | `doubled$`, `userName$` |
389
+ | Pool | `Pool` | `userPool` |
390
+ | Service | `Service` | `authService` |
391
+ | Store | `Store` | `authStore` |
392
+
393
+ **Why no `Async$`?** Atomirx abstracts async/sync — you don't care in SelectContext (Suspense handles it), and services receive values as parameters.
394
+
395
+ ### File Structure
396
+
397
+ ```
398
+ src/
399
+ ├── services/ # Stateless
400
+ │ ├── auth/
401
+ │ │ └── auth.service.ts
402
+ │ └── crypto/
403
+ │ └── crypto.service.ts
404
+ └── stores/ # Stateful
405
+ ├── auth.store.ts
406
+ └── todos.store.ts
407
+ ```
@@ -0,0 +1,309 @@
1
+ # SelectContext API
2
+
3
+ SelectContext provides methods for `derived`, `effect`, `useSelector`, and `rx`.
4
+
5
+ ## Methods
6
+
7
+ | Method | Returns | Description |
8
+ | --------------- | -------------------------- | ------------------------------ |
9
+ | `read(atom)` | `T` | Get value, suspends if loading |
10
+ | `ready(atom)` | `T` (non-nullable) | Block until truthy |
11
+ | `state(atom)` | `AtomState<T>` | Get state without suspending |
12
+ | `untrack(atom)` | `T` | Read without tracking dep |
13
+ | `untrack(fn)` | `T` | Execute fn without tracking |
14
+ | `safe(fn)` | `[error?, value?]` | Catch errors, rethrow Promise |
15
+ | `all(atoms)` | `T[]` | Wait for all |
16
+ | `any(record)` | `{ key, value }` | First to resolve |
17
+ | `race(record)` | `{ key, value }` | First to settle |
18
+ | `settled()` | `SettledResult<T>[]` | All with status |
19
+ | `from(pool)` | `ScopedAtom<T>` | Get atom from pool |
20
+ | `track(atom)` | `void` | Track without reading |
21
+ | `and(conds)` | `boolean` | Logical AND |
22
+ | `or(conds)` | `boolean` | Logical OR |
23
+
24
+ ## CRITICAL Rules
25
+
26
+ ### Sync Access Only
27
+
28
+ **All methods MUST be called synchronously:**
29
+
30
+ ```typescript
31
+ // ❌ FORBIDDEN
32
+ derived(({ read }) => {
33
+ setTimeout(() => read(atom$), 100); // Throws
34
+ return "value";
35
+ });
36
+
37
+ // ✅ REQUIRED
38
+ derived(({ read }) => {
39
+ const value = read(atom$); // Sync call
40
+ return value;
41
+ });
42
+ ```
43
+
44
+ ### read() + Suspense
45
+
46
+ `read()` suspends (throws Promise) when atom loading:
47
+
48
+ ```tsx
49
+ // Wrap with Suspense
50
+ <Suspense fallback={<Loading />}>
51
+ <Component />
52
+ </Suspense>
53
+
54
+ function Component() {
55
+ const data = useSelector(({ read }) => read(asyncAtom$)); // May suspend
56
+ return <div>{data}</div>;
57
+ }
58
+ ```
59
+
60
+ ### from() Returns ScopedAtom
61
+
62
+ **MUST use with `read()`, NEVER `.get()`:**
63
+
64
+ ```typescript
65
+ // ❌ FORBIDDEN
66
+ derived(({ from }) => {
67
+ const user$ = from(userPool, "user-1");
68
+ return user$.get(); // THROWS
69
+ });
70
+
71
+ // ✅ REQUIRED
72
+ derived(({ read, from }) => {
73
+ const user$ = from(userPool, "user-1");
74
+ return read(user$);
75
+ });
76
+ ```
77
+
78
+ ### untrack() — Read Without Tracking
79
+
80
+ **Read atoms or execute functions without creating dependencies.**
81
+
82
+ Use `untrack()` when you need a value but don't want re-computation when it changes.
83
+
84
+ ```typescript
85
+ // Form 1: Pass atom directly
86
+ const combined$ = derived(({ read, untrack }) => {
87
+ const count = read(count$); // ✅ Tracked — re-computes on change
88
+ const config = untrack(config$); // ❌ NOT tracked — no re-compute
89
+ return count * config.multiplier;
90
+ });
91
+
92
+ // Form 2: Pass function for multiple reads
93
+ const snapshot$ = derived(({ read, untrack }) => {
94
+ const liveData = read(liveData$); // Tracked
95
+ const snapshot = untrack(() => {
96
+ // None of these create dependencies
97
+ return { a: read(a$), b: read(b$), c: read(c$) };
98
+ });
99
+ return { liveData, snapshot };
100
+ });
101
+ ```
102
+
103
+ **When to use:**
104
+
105
+ | Scenario | Use |
106
+ | -------- | --- |
107
+ | Need latest value, re-compute on change | `read()` |
108
+ | Need value once, ignore future changes | `untrack()` |
109
+ | Initial value / config that rarely changes | `untrack()` |
110
+ | Snapshot of multiple atoms | `untrack(() => ...)` |
111
+
112
+ **Flow Diagram:**
113
+
114
+ ```
115
+ untrack(input)
116
+
117
+
118
+ Is input an Atom?
119
+
120
+ ┌───┴───┐
121
+ │Yes │No (function)
122
+ ▼ ▼
123
+ Read Disable tracking
124
+ value │
125
+ (no ▼
126
+ track) Execute fn()
127
+ │ │
128
+ │ ▼
129
+ │ Re-enable tracking
130
+ │ │
131
+ └───────┴──→ Return value
132
+ ```
133
+
134
+ ## Type Definitions
135
+
136
+ ```typescript
137
+ interface SelectContext {
138
+ read<T>(atom: ReadableAtom<T>): T;
139
+ ready<T>(atom: ReadableAtom<T | undefined | null>): T;
140
+ state<T>(atom: ReadableAtom<T>): AtomState<T>;
141
+ untrack<T>(atom: ReadableAtom<T>): T;
142
+ untrack<T>(fn: () => T): T;
143
+ safe<T>(fn: () => T): [unknown, undefined] | [undefined, T];
144
+ all<T extends readonly ReadableAtom<unknown>[]>(atoms: T): MapAtomValues<T>;
145
+ any<T extends Record<string, ReadableAtom<unknown>>>(atoms: T): { key: keyof T; value: T[keyof T] };
146
+ race<T extends Record<string, ReadableAtom<unknown>>>(atoms: T): { key: keyof T; value: T[keyof T] };
147
+ settled<T extends readonly ReadableAtom<unknown>[]>(atoms: T): MapSettledResults<T>;
148
+ from<P, T>(pool: Pool<P, T>, params: P): ScopedAtom<T>;
149
+ track(atom: ReadableAtom<unknown>): void;
150
+ and(conditions: Condition[]): boolean;
151
+ or(conditions: Condition[]): boolean;
152
+ }
153
+
154
+ type AtomState<T> =
155
+ | { status: "ready"; value: T }
156
+ | { status: "error"; error: unknown }
157
+ | { status: "loading"; promise: Promise<T> };
158
+
159
+ type SettledResult<T> =
160
+ | { status: "ready"; value: T }
161
+ | { status: "error"; error: unknown };
162
+
163
+ type Condition = boolean | ReadableAtom<unknown> | (() => boolean | ReadableAtom<unknown>);
164
+ ```
165
+
166
+ ## EffectContext
167
+
168
+ Effects get additional methods:
169
+
170
+ ```typescript
171
+ interface EffectContext extends SelectContext {
172
+ onCleanup: (fn: VoidFunction) => void;
173
+ signal: AbortSignal;
174
+ }
175
+ ```
176
+
177
+ ## Usage in Primitives
178
+
179
+ ### derived
180
+
181
+ ```typescript
182
+ const doubled$ = derived(({ read }) => read(count$) * 2);
183
+
184
+ const combined$ = derived(({ all }) => {
185
+ const [a, b, c] = all([atom1$, atom2$, atom3$]);
186
+ return a + b + c;
187
+ });
188
+
189
+ const guarded$ = derived(({ ready, read, from }) => {
190
+ const id = ready(currentId$);
191
+ return read(from(userPool, id));
192
+ });
193
+ ```
194
+
195
+ ### effect
196
+
197
+ ```typescript
198
+ effect(({ read, onCleanup, signal }) => {
199
+ const id = read(userId$);
200
+
201
+ fetch(`/api/user/${id}`, { signal })
202
+ .then(r => r.json())
203
+ .then(data => userDetails$.set(data))
204
+ .catch(err => {
205
+ if (err.name !== "AbortError") console.error(err);
206
+ });
207
+
208
+ const sub = eventBus.subscribe("update", handler);
209
+ onCleanup(() => sub.unsubscribe());
210
+ }, { meta: { key: "fetch.user" } });
211
+ ```
212
+
213
+ ### useSelector
214
+
215
+ ```tsx
216
+ const data = useSelector(({ read, safe }) => {
217
+ const [err, value] = safe(() => JSON.parse(read(rawJson$)));
218
+ return err ? { error: err.message } : { data: value };
219
+ });
220
+ ```
221
+
222
+ ### rx
223
+
224
+ ```tsx
225
+ {rx(({ read, state }) => {
226
+ const userState = state(user$);
227
+ if (userState.status === "loading") return <Skeleton />;
228
+ if (userState.status === "error") return <ErrorMsg />;
229
+ return <UserCard user={userState.value} />;
230
+ })}
231
+ ```
232
+
233
+ ## use() — Composable Selectors
234
+
235
+ Split and reuse selection logic with `use()`:
236
+
237
+ ```typescript
238
+ // Reusable selector functions
239
+ const selectProduct = ({ read }: SelectContext) => read(product$);
240
+ const selectUser = ({ read }: SelectContext) => read(user$);
241
+ const selectCart = ({ read, from }: SelectContext) => {
242
+ const userId = read(userId$);
243
+ return read(from(cartPool, userId));
244
+ };
245
+
246
+ // Compose in derived
247
+ const checkout$ = derived(({ use }) => {
248
+ const product = use(selectProduct);
249
+ const user = use(selectUser);
250
+ const cart = use(selectCart);
251
+ return { product, user, cart };
252
+ });
253
+
254
+ // Reuse in effect
255
+ effect(({ use }) => {
256
+ const user = use(selectUser);
257
+ analytics.identify(user.id);
258
+ });
259
+
260
+ // Reuse in useSelector
261
+ const { product, user } = useSelector(({ use }) => ({
262
+ product: use(selectProduct),
263
+ user: use(selectUser),
264
+ }));
265
+ ```
266
+
267
+ **Benefits:**
268
+ - Reusable selection logic across derived/effect/useSelector
269
+ - Cleaner, more readable selectors
270
+ - Easy to test individual selectors
271
+ - Single source of truth for data access patterns
272
+
273
+ ## .get() vs read()
274
+
275
+ | Context | Use | Why |
276
+ | --------------------------- | ---------- | ---------------------------- |
277
+ | Inside selector callback | `read()` | Tracks dependencies |
278
+ | setTimeout/setInterval | `.get()` | Outside reactive context |
279
+ | Event handlers | `.get()` | Outside reactive context |
280
+ | After await | `.get()` | Context no longer valid |
281
+
282
+ ```typescript
283
+ effect(({ read }) => {
284
+ const config = read(config$); // ✅ read() — tracked
285
+
286
+ setTimeout(() => {
287
+ const value = count$.get(); // ✅ .get() — outside context
288
+ console.log(value);
289
+ }, 1000);
290
+ });
291
+
292
+ // In event handler
293
+ const handleClick = () => {
294
+ const current = count$.get(); // ✅ .get() — not in selector
295
+ console.log(current);
296
+ };
297
+ ```
298
+
299
+ ## Best Practices
300
+
301
+ 1. **Group reads** in useSelector
302
+ 2. **Use `safe()`** for error handling (not try/catch)
303
+ 3. **Use `ready()`** for optional params
304
+ 4. **Use `state()`** when you need loading/error states without Suspense
305
+ 5. **Use `all()`** for parallel async atoms
306
+ 6. **Use `settled()`** for graceful degradation
307
+ 7. **Use `from()`** only with `read()` — never `.get()`
308
+ 8. **Use `read()`** inside selectors, `.get()` outside
309
+ 9. **Use `untrack()`** when you need a value but don't want re-computation on change