atomirx 0.0.8 → 0.1.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.
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,426 @@
1
+ # Effect Patterns
2
+
3
+ Effects run side effects when atoms change. Handles sync/async atoms, executes synchronously.
4
+
5
+ ```typescript
6
+ const user$ = atom(fetchUser());
7
+
8
+ effect(
9
+ ({ read }) => {
10
+ const user = read(user$); // Suspends until resolved
11
+ console.log(`User: ${user.name}`);
12
+ localStorage.setItem("lastUser", user.id);
13
+ },
14
+ { meta: { key: "log.user" } }
15
+ );
16
+ ```
17
+
18
+ ## When to Use
19
+
20
+ **Use `effect()` for:**
21
+
22
+ - Logging, persisting, syncing
23
+ - Triggering events, updating atoms
24
+ - Reacting to state changes with side effects
25
+ - Kicking off async work (fire-and-forget)
26
+
27
+ **NEVER use for:**
28
+
29
+ - User-triggered actions → plain function with `.set()`
30
+ - Computed values → `derived()`
31
+ - Operations needing return value → `derived()`
32
+
33
+ ## Async in Effects
34
+
35
+ Effects are **sync** but CAN trigger async work:
36
+
37
+ ```typescript
38
+ // ✅ Fire-and-forget async call
39
+ effect(
40
+ ({ read }) => {
41
+ const productId = read(currentProductId$);
42
+ fetchAnalytics(productId); // No await, just trigger
43
+ },
44
+ { meta: { key: "analytics.product" } }
45
+ );
46
+
47
+ // ✅ Assign Promise to atom (atom stores the Promise)
48
+ effect(
49
+ ({ read }) => {
50
+ const productId = read(currentProductId$);
51
+ productDetails$.set(fetchProductDetails(productId)); // Promise assigned
52
+ },
53
+ { meta: { key: "fetch.productDetails" } }
54
+ );
55
+
56
+ // ✅ Async with signal for cancellation
57
+ effect(
58
+ ({ read, signal }) => {
59
+ const userId = read(userId$);
60
+ fetch(`/api/user/${userId}`, { signal })
61
+ .then((r) => r.json())
62
+ .then((data) => userDetails$.set(data))
63
+ .catch((err) => {
64
+ if (err.name !== "AbortError") console.error(err);
65
+ });
66
+ },
67
+ { meta: { key: "fetch.user" } }
68
+ );
69
+ ```
70
+
71
+ | Pattern | Description |
72
+ | ------- | ----------- |
73
+ | `fetchSomething()` | Fire-and-forget, no await |
74
+ | `atom$.set(fetchX())` | Store Promise in atom |
75
+ | `fetch(..., { signal })` | Cancelable with AbortSignal |
76
+
77
+ ## Features
78
+
79
+ | Feature | Description |
80
+ | ---------------- | --------------------------------- |
81
+ | Auto cleanup | Previous cleanup runs first |
82
+ | Suspense-aware | Waits for async atoms |
83
+ | Batched updates | Atom updates batched |
84
+ | Conditional deps | Only tracks accessed atoms |
85
+ | Eager execution | Runs immediately (unlike derived) |
86
+
87
+ ## Core API
88
+
89
+ ```typescript
90
+ interface Effect {
91
+ dispose: VoidFunction;
92
+ meta?: EffectMeta;
93
+ }
94
+
95
+ interface EffectContext extends SelectContext {
96
+ onCleanup: (fn: VoidFunction) => void;
97
+ signal: AbortSignal;
98
+ }
99
+ ```
100
+
101
+ ## CRITICAL Rules
102
+
103
+ ### MUST Be Sync
104
+
105
+ ```typescript
106
+ // ❌ FORBIDDEN
107
+ effect(async ({ read }) => {
108
+ const data = await fetch("/api");
109
+ });
110
+
111
+ // ✅ REQUIRED
112
+ const data$ = atom(fetch("/api").then((r) => r.json()));
113
+ effect(({ read }) => console.log(read(data$)));
114
+ ```
115
+
116
+ ### NEVER try/catch — Use safe()
117
+
118
+ ```typescript
119
+ // ❌ FORBIDDEN
120
+ effect(({ read }) => {
121
+ try {
122
+ riskyOp(read(asyncAtom$));
123
+ } catch (e) {
124
+ console.error(e);
125
+ } // Catches Promise!
126
+ });
127
+
128
+ // ✅ REQUIRED
129
+ effect(({ read, safe }) => {
130
+ const [err, data] = safe(() => riskyOp(read(asyncAtom$)));
131
+ if (err) console.error("Failed:", err);
132
+ });
133
+ ```
134
+
135
+ ### MUST Define meta.key
136
+
137
+ ```typescript
138
+ // ✅ REQUIRED
139
+ effect(({ read }) => localStorage.setItem("count", String(read(count$))), {
140
+ meta: { key: "persist.count" },
141
+ });
142
+
143
+ // ❌ FORBIDDEN
144
+ effect(({ read }) => localStorage.setItem("count", String(read(count$))));
145
+ ```
146
+
147
+ ### Single Workflow
148
+
149
+ ```typescript
150
+ // ❌ FORBIDDEN — multiple workflows
151
+ effect(({ read }) => {
152
+ localStorage.setItem("count", String(read(count$)));
153
+ syncToServer(read(count$));
154
+ trackEvent("count_changed", read(count$));
155
+ });
156
+
157
+ // ✅ REQUIRED — separate effects
158
+ effect(({ read }) => localStorage.setItem("count", String(read(count$))), {
159
+ meta: { key: "persist.count" },
160
+ });
161
+ effect(({ read }) => syncToServer(read(count$)), {
162
+ meta: { key: "sync.count" },
163
+ });
164
+ effect(({ read }) => trackEvent("count_changed", read(count$)), {
165
+ meta: { key: "analytics.count" },
166
+ });
167
+ ```
168
+
169
+ ## Cleanup Patterns
170
+
171
+ ### Basic
172
+
173
+ ```typescript
174
+ effect(({ read, onCleanup }) => {
175
+ const id = setInterval(() => console.log(read(count$)), 1000);
176
+ onCleanup(() => clearInterval(id));
177
+ });
178
+ ```
179
+
180
+ ### Multiple (FIFO)
181
+
182
+ ```typescript
183
+ effect(({ read, onCleanup }) => {
184
+ const sub1 = eventBus.subscribe("a", handler1);
185
+ const sub2 = eventBus.subscribe("b", handler2);
186
+ onCleanup(() => sub1.unsubscribe()); // First
187
+ onCleanup(() => sub2.unsubscribe()); // Second
188
+ });
189
+ ```
190
+
191
+ ### AbortSignal
192
+
193
+ ```typescript
194
+ effect(({ read, signal }) => {
195
+ const userId = read(userId$);
196
+ fetch(`/api/user/${userId}`, { signal })
197
+ .then((r) => r.json())
198
+ .then((data) => userDetails$.set(data))
199
+ .catch((err) => {
200
+ if (err.name !== "AbortError") console.error(err);
201
+ });
202
+ });
203
+ ```
204
+
205
+ ### WebSocket
206
+
207
+ ```typescript
208
+ effect(
209
+ ({ read, onCleanup }) => {
210
+ const socket = new WebSocket(read(wsUrl$));
211
+ socket.onmessage = (e) => messages$.set((p) => [...p, e.data]);
212
+ onCleanup(() => socket.close());
213
+ },
214
+ { meta: { key: "ws.connection" } }
215
+ );
216
+ ```
217
+
218
+ ## Common Patterns
219
+
220
+ ### LocalStorage
221
+
222
+ ```typescript
223
+ effect(
224
+ ({ read }) => {
225
+ localStorage.setItem("settings", JSON.stringify(read(settings$)));
226
+ },
227
+ { meta: { key: "persist.settings" } }
228
+ );
229
+ ```
230
+
231
+ ### Analytics
232
+
233
+ ```typescript
234
+ effect(
235
+ ({ read }) => {
236
+ analytics.track("page_view", { page: read(currentPage$) });
237
+ },
238
+ { meta: { key: "analytics.pageView" } }
239
+ );
240
+ ```
241
+
242
+ ### Debug (Dev Only)
243
+
244
+ ```typescript
245
+ if (process.env.NODE_ENV === "development") {
246
+ effect(
247
+ ({ read }) => {
248
+ console.log("[DEBUG]", { user: read(user$), cart: read(cart$) });
249
+ },
250
+ { meta: { key: "debug.state" } }
251
+ );
252
+ }
253
+ ```
254
+
255
+ ### Conditional
256
+
257
+ ```typescript
258
+ effect(
259
+ ({ read }) => {
260
+ if (!read(featureFlag$)) return;
261
+ syncToExternalService(read(data$));
262
+ },
263
+ { meta: { key: "sync.external" } }
264
+ );
265
+ ```
266
+
267
+ ### Debounced
268
+
269
+ ```typescript
270
+ effect(
271
+ ({ read, onCleanup }) => {
272
+ const data = read(formData$);
273
+ const id = setTimeout(
274
+ () => localStorage.setItem("draft", JSON.stringify(data)),
275
+ 500
276
+ );
277
+ onCleanup(() => clearTimeout(id));
278
+ },
279
+ { meta: { key: "persist.formDraft" } }
280
+ );
281
+ ```
282
+
283
+ ### Cross-Tab Sync
284
+
285
+ ```typescript
286
+ effect(
287
+ ({ read, onCleanup }) => {
288
+ const settings = read(settings$);
289
+ const channel = new BroadcastChannel("settings");
290
+ channel.postMessage(settings);
291
+
292
+ const handler = (e: MessageEvent) => settings$.set(e.data);
293
+ channel.addEventListener("message", handler);
294
+ onCleanup(() => {
295
+ channel.removeEventListener("message", handler);
296
+ channel.close();
297
+ });
298
+ },
299
+ { meta: { key: "sync.crossTab" } }
300
+ );
301
+ ```
302
+
303
+ ### Document Title
304
+
305
+ ```typescript
306
+ effect(
307
+ ({ read }) => {
308
+ const count = read(unreadCount$);
309
+ document.title = count > 0 ? `(${count}) My App` : "My App";
310
+ },
311
+ { meta: { key: "ui.documentTitle" } }
312
+ );
313
+ ```
314
+
315
+ ### Non-Reactive Config (untrack)
316
+
317
+ Read config/settings without triggering re-run when they change:
318
+
319
+ ```typescript
320
+ // Effect only re-runs when userId$ changes, NOT when apiConfig$ changes
321
+ effect(
322
+ ({ read, untrack }) => {
323
+ const userId = read(userId$); // Tracked — triggers re-run
324
+ const config = untrack(apiConfig$); // NOT tracked — no re-run
325
+ fetch(`${config.baseUrl}/users/${userId}`);
326
+ },
327
+ { meta: { key: "fetch.user" } }
328
+ );
329
+
330
+ // Snapshot multiple atoms for logging without tracking all of them
331
+ effect(
332
+ ({ read, untrack }) => {
333
+ const currentPage = read(currentPage$); // Only track page changes
334
+ const snapshot = untrack(() => ({
335
+ user: read(user$),
336
+ settings: read(settings$),
337
+ cart: read(cart$),
338
+ }));
339
+ analytics.track("page_view", { page: currentPage, ...snapshot });
340
+ },
341
+ { meta: { key: "analytics.pageView" } }
342
+ );
343
+ ```
344
+
345
+ | Use Case | Method |
346
+ | -------- | ------ |
347
+ | Value that triggers effect | `read()` |
348
+ | Config/reference values | `untrack()` |
349
+ | Snapshot for logging | `untrack(() => ...)` |
350
+
351
+ ## Options
352
+
353
+ ```typescript
354
+ interface EffectOptions {
355
+ meta?: { key?: string };
356
+ onError?: (error: unknown) => void;
357
+ }
358
+
359
+ effect(({ read }) => riskyOp(read(data$)), {
360
+ meta: { key: "risky.op" },
361
+ onError: (err) => Sentry.captureException(err),
362
+ });
363
+ ```
364
+
365
+ ## Effect vs Derived
366
+
367
+ | Aspect | effect() | derived() |
368
+ | ------------ | ------------- | -------------- |
369
+ | Returns | void | Computed value |
370
+ | Execution | Eager | Lazy |
371
+ | Purpose | Side effects | Transform data |
372
+ | **Can set** | **✅ Yes** | **❌ NEVER** |
373
+ | Subscription | Always active | When accessed |
374
+
375
+ ## Lifecycle
376
+
377
+ ```
378
+ Created → Initial run → Dep changed → Cleanup → Re-run → ... → dispose() → Final cleanup → Stopped
379
+ ```
380
+
381
+ ## Testing
382
+
383
+ ```typescript
384
+ it("should persist to localStorage", () => {
385
+ const count$ = atom(0, { meta: { key: "test.count" } });
386
+ const e = effect(
387
+ ({ read }) => localStorage.setItem("count", String(read(count$))),
388
+ {
389
+ meta: { key: "test.persist" },
390
+ }
391
+ );
392
+
393
+ expect(localStorage.getItem("count")).toBe("0");
394
+ count$.set(5);
395
+ expect(localStorage.getItem("count")).toBe("5");
396
+ e.dispose();
397
+ });
398
+
399
+ it("should cleanup on dispose", () => {
400
+ const cleanup = vi.fn();
401
+ const count$ = atom(0);
402
+ const e = effect(({ read, onCleanup }) => {
403
+ read(count$);
404
+ onCleanup(cleanup);
405
+ });
406
+
407
+ expect(cleanup).not.toHaveBeenCalled();
408
+ e.dispose();
409
+ expect(cleanup).toHaveBeenCalledOnce();
410
+ });
411
+ ```
412
+
413
+ ## When to Use
414
+
415
+ ✅ **Use effect:**
416
+
417
+ - Sync to storage, analytics, logging
418
+ - Manage subscriptions (WS, events)
419
+ - Update DOM properties
420
+ - Mutate atoms based on computed
421
+
422
+ ❌ **NEVER use effect:**
423
+
424
+ - User triggers action → plain function
425
+ - Computing derived values → `derived()`
426
+ - Need return value → `derived()`
@@ -0,0 +1,140 @@
1
+ # Error Handling: safe() Not try/catch
2
+
3
+ ## The Problem
4
+
5
+ `read()` uses **Suspense**: loading atoms throw Promise. try/catch catches the Promise:
6
+
7
+ ```typescript
8
+ // ❌ WRONG — breaks Suspense
9
+ const data$ = derived(({ read }) => {
10
+ try {
11
+ const user = read(asyncUser$); // Throws Promise when loading
12
+ return processUser(user);
13
+ } catch (e) {
14
+ // Catches BOTH:
15
+ // 1. Promise (loading) — breaks Suspense
16
+ // 2. Actual errors
17
+ return null;
18
+ }
19
+ });
20
+ ```
21
+
22
+ **Problems:**
23
+ - Loading state lost
24
+ - No Suspense fallback
25
+ - Can't distinguish loading from error
26
+
27
+ ## The Solution: safe()
28
+
29
+ `safe()` catches errors, **re-throws Promises**:
30
+
31
+ ```typescript
32
+ // ✅ CORRECT
33
+ const data$ = derived(({ read, safe }) => {
34
+ const [err, user] = safe(() => {
35
+ const raw = read(asyncUser$); // Can throw Promise ✓
36
+ return processUser(raw); // Can throw Error ✓
37
+ });
38
+
39
+ if (err) return { error: err.message };
40
+ return { user };
41
+ });
42
+ ```
43
+
44
+ ## How safe() Works
45
+
46
+ | Scenario | `try/catch` | `safe()` |
47
+ | ---------- | ------------------ | --------------------------- |
48
+ | Loading | ❌ Catches Promise | ✅ Re-throws → Suspense |
49
+ | Error | ✅ Catches | ✅ Returns `[error, undef]` |
50
+ | Success | ✅ Returns | ✅ Returns `[undef, value]` |
51
+
52
+ ## Use Cases
53
+
54
+ ### Parsing/Validation
55
+
56
+ ```typescript
57
+ const parsed$ = derived(({ read, safe }) => {
58
+ const [err, config] = safe(() => {
59
+ const raw = read(rawConfig$);
60
+ return JSON.parse(raw);
61
+ });
62
+
63
+ if (err) return { valid: false, error: "Invalid JSON" };
64
+ return { valid: true, config };
65
+ });
66
+ ```
67
+
68
+ ### Graceful Degradation
69
+
70
+ ```typescript
71
+ const dashboard$ = derived(({ read, safe }) => {
72
+ const user = read(user$); // Required
73
+
74
+ const [err1, analytics] = safe(() => read(analytics$));
75
+ const [err2, notifications] = safe(() => read(notifications$));
76
+
77
+ return {
78
+ user,
79
+ analytics: err1 ? null : analytics,
80
+ notifications: err2 ? [] : notifications,
81
+ errors: [err1, err2].filter(Boolean),
82
+ };
83
+ });
84
+ ```
85
+
86
+ ### Effects
87
+
88
+ ```typescript
89
+ effect(({ read, safe }) => {
90
+ const [err, data] = safe(() => {
91
+ const raw = read(asyncData$);
92
+ return transformData(raw);
93
+ });
94
+
95
+ if (err) {
96
+ console.error("Failed:", err);
97
+ return;
98
+ }
99
+ saveToLocalStorage(data);
100
+ });
101
+ ```
102
+
103
+ ### React Components
104
+
105
+ ```tsx
106
+ function UserProfile() {
107
+ const result = useSelector(({ read, safe }) => {
108
+ const [err, user] = safe(() => read(user$));
109
+ return { err, user };
110
+ });
111
+
112
+ if (result.err) return <ErrorMessage error={result.err} />;
113
+ return <Profile user={result.user} />;
114
+ }
115
+ ```
116
+
117
+ ### With rx()
118
+
119
+ ```tsx
120
+ <Suspense fallback={<Loading />}>
121
+ {rx(({ read, safe }) => {
122
+ const [err, posts] = safe(() => read(posts$));
123
+ if (err) return <ErrorBanner message="Failed to load" />;
124
+ return posts.map((p) => <PostCard key={p.id} post={p} />);
125
+ })}
126
+ </Suspense>
127
+ ```
128
+
129
+ ## Alternative: state()
130
+
131
+ For manual loading handling (no Suspense):
132
+
133
+ ```typescript
134
+ const result = useSelector(({ state }) => state(user$));
135
+ // { status: "loading" | "ready" | "error", value?, error? }
136
+
137
+ if (result.status === "loading") return <Loading />;
138
+ if (result.status === "error") return <Error error={result.error} />;
139
+ return <User data={result.value} />;
140
+ ```