atomirx 0.0.2 → 0.0.4

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 (53) hide show
  1. package/README.md +866 -159
  2. package/dist/core/atom.d.ts +83 -6
  3. package/dist/core/batch.d.ts +3 -3
  4. package/dist/core/derived.d.ts +55 -21
  5. package/dist/core/effect.d.ts +47 -51
  6. package/dist/core/getAtomState.d.ts +29 -0
  7. package/dist/core/promiseCache.d.ts +23 -32
  8. package/dist/core/select.d.ts +208 -29
  9. package/dist/core/types.d.ts +55 -19
  10. package/dist/core/withReady.d.ts +69 -0
  11. package/dist/index-CqO6BDwj.cjs +1 -0
  12. package/dist/index-D8RDOTB_.js +1319 -0
  13. package/dist/index.cjs +1 -1
  14. package/dist/index.d.ts +9 -7
  15. package/dist/index.js +12 -10
  16. package/dist/react/index.cjs +10 -10
  17. package/dist/react/index.d.ts +2 -1
  18. package/dist/react/index.js +423 -379
  19. package/dist/react/rx.d.ts +114 -25
  20. package/dist/react/useAction.d.ts +5 -4
  21. package/dist/react/{useValue.d.ts → useSelector.d.ts} +56 -25
  22. package/dist/react/useSelector.test.d.ts +1 -0
  23. package/package.json +1 -1
  24. package/src/core/atom.test.ts +307 -43
  25. package/src/core/atom.ts +143 -21
  26. package/src/core/batch.test.ts +10 -10
  27. package/src/core/batch.ts +3 -3
  28. package/src/core/derived.test.ts +727 -72
  29. package/src/core/derived.ts +141 -73
  30. package/src/core/effect.test.ts +259 -39
  31. package/src/core/effect.ts +62 -85
  32. package/src/core/getAtomState.ts +69 -0
  33. package/src/core/promiseCache.test.ts +5 -3
  34. package/src/core/promiseCache.ts +76 -71
  35. package/src/core/select.ts +405 -130
  36. package/src/core/selector.test.ts +574 -32
  37. package/src/core/types.ts +54 -26
  38. package/src/core/withReady.test.ts +360 -0
  39. package/src/core/withReady.ts +127 -0
  40. package/src/core/withUse.ts +1 -1
  41. package/src/index.test.ts +4 -4
  42. package/src/index.ts +11 -6
  43. package/src/react/index.ts +2 -1
  44. package/src/react/rx.test.tsx +173 -18
  45. package/src/react/rx.tsx +274 -43
  46. package/src/react/useAction.test.ts +12 -14
  47. package/src/react/useAction.ts +11 -9
  48. package/src/react/{useValue.test.ts → useSelector.test.ts} +16 -16
  49. package/src/react/{useValue.ts → useSelector.ts} +64 -33
  50. package/v2.md +44 -44
  51. package/dist/index-2ok7ilik.js +0 -1217
  52. package/dist/index-B_5SFzfl.cjs +0 -1
  53. /package/dist/{react/useValue.test.d.ts → core/withReady.test.d.ts} +0 -0
package/README.md CHANGED
@@ -24,8 +24,6 @@ We can't solve every use case, but in the spirit of [`create-react-app`](https:/
24
24
  - [Purpose](#purpose)
25
25
  - [Table of Contents](#table-of-contents)
26
26
  - [Installation](#installation)
27
- - [Using Create React App](#using-create-react-app)
28
- - [Adding to an Existing Project](#adding-to-an-existing-project)
29
27
  - [Why atomirx?](#why-atomirx)
30
28
  - [The Problem](#the-problem)
31
29
  - [The Solution](#the-solution)
@@ -36,11 +34,23 @@ We can't solve every use case, but in the spirit of [`create-react-app`](https:/
36
34
  - [Getting Started](#getting-started)
37
35
  - [Basic Example: Counter](#basic-example-counter)
38
36
  - [React Example: Todo App](#react-example-todo-app)
39
- - [Code Conventions](#code-conventions)
37
+ - [Patterns \& Best Practices](#patterns--best-practices)
40
38
  - [Naming: The `$` Suffix](#naming-the--suffix)
41
39
  - [When to Use Each Primitive](#when-to-use-each-primitive)
42
40
  - [Atom Storage: Stable Scopes Only](#atom-storage-stable-scopes-only)
41
+ - [Error Handling: Use `safe()` Not try/catch](#error-handling-use-safe-not-trycatch)
42
+ - [The Problem with try/catch](#the-problem-with-trycatch)
43
+ - [The Solution: safe()](#the-solution-safe)
44
+ - [Use Cases for safe()](#use-cases-for-safe)
45
+ - [SelectContext Methods: Synchronous Only](#selectcontext-methods-synchronous-only)
43
46
  - [Complete Example: Todo App with Async](#complete-example-todo-app-with-async)
47
+ - [Deferred Entity Loading with `ready()`](#deferred-entity-loading-with-ready)
48
+ - [The Pattern](#the-pattern)
49
+ - [How It Works](#how-it-works)
50
+ - [Component Usage](#component-usage)
51
+ - [Benefits](#benefits)
52
+ - [When to Use `ready()`](#when-to-use-ready)
53
+ - [Important Notes](#important-notes)
44
54
  - [Usage Guide](#usage-guide)
45
55
  - [Atoms: The Foundation](#atoms-the-foundation)
46
56
  - [Creating Atoms](#creating-atoms)
@@ -56,7 +66,6 @@ We can't solve every use case, but in the spirit of [`create-react-app`](https:/
56
66
  - [Effects: Side Effect Management](#effects-side-effect-management)
57
67
  - [Basic Effects](#basic-effects)
58
68
  - [Effects with Cleanup](#effects-with-cleanup)
59
- - [Effects with Error Handling](#effects-with-error-handling)
60
69
  - [Effects with Multiple Dependencies](#effects-with-multiple-dependencies)
61
70
  - [Async Patterns](#async-patterns)
62
71
  - [`all()` - Wait for All (like Promise.all)](#all---wait-for-all-like-promiseall)
@@ -74,9 +83,12 @@ We can't solve every use case, but in the spirit of [`create-react-app`](https:/
74
83
  - [Multiple Middleware Example](#multiple-middleware-example)
75
84
  - [Hook Info Types](#hook-info-types)
76
85
  - [React Integration](#react-integration)
77
- - [useValue Hook](#usevalue-hook)
86
+ - [useSelector Hook](#useselector-hook)
78
87
  - [Custom Equality](#custom-equality)
88
+ - [Why useSelector is Powerful](#why-useselector-is-powerful)
79
89
  - [Reactive Components with rx](#reactive-components-with-rx)
90
+ - [Inline Loading and Error Handling](#inline-loading-and-error-handling)
91
+ - [Selector Memoization with `deps`](#selector-memoization-with-deps)
80
92
  - [Async Actions with useAction](#async-actions-with-useaction)
81
93
  - [Eager Execution](#eager-execution)
82
94
  - [useAction API](#useaction-api)
@@ -93,8 +105,9 @@ We can't solve every use case, but in the spirit of [`create-react-app`](https:/
93
105
  - [`define<T>(factory, options?)`](#definetfactory-options)
94
106
  - [`isAtom(value)`](#isatomvalue)
95
107
  - [SelectContext API](#selectcontext-api)
108
+ - [`state()` - Get Async State Without Throwing](#state---get-async-state-without-throwing)
96
109
  - [React API](#react-api)
97
- - [`useValue`](#usevalue)
110
+ - [`useSelector`](#useselector)
98
111
  - [`rx`](#rx)
99
112
  - [`useAction`](#useaction)
100
113
  - [`useStable`](#usestable)
@@ -109,16 +122,6 @@ We can't solve every use case, but in the spirit of [`create-react-app`](https:/
109
122
 
110
123
  ## Installation
111
124
 
112
- ### Using Create React App
113
-
114
- The fastest way to get started is using our official template:
115
-
116
- ```bash
117
- npx create-react-app my-app --template atomirx
118
- ```
119
-
120
- ### Adding to an Existing Project
121
-
122
125
  atomirx is available as a package on NPM for use with a module bundler or in a Node application:
123
126
 
124
127
  ```bash
@@ -184,8 +187,8 @@ atomirx includes these APIs:
184
187
 
185
188
  ### React Bindings (`atomirx/react`)
186
189
 
187
- - **`useValue()`**: Subscribe to atoms with automatic re-rendering
188
- - **`rx()`**: Inline reactive components for fine-grained updates
190
+ - **`useSelector()`**: Subscribe to atoms with automatic re-rendering (Suspense-based)
191
+ - **`rx()`**: Inline reactive components with optional loading/error handlers
189
192
  - **`useAction()`**: Handle async operations with loading/error states
190
193
  - **`useStable()`**: Stabilize object/array/callback references
191
194
 
@@ -200,16 +203,16 @@ import { atom, derived, effect } from "atomirx";
200
203
  const count$ = atom(0);
201
204
 
202
205
  // Step 2: Create derived state (computed values)
203
- const doubled$ = derived(({ get }) => get(count$) * 2);
204
- const message$ = derived(({ get }) => {
205
- const count = get(count$);
206
+ const doubled$ = derived(({ read }) => read(count$) * 2);
207
+ const message$ = derived(({ read }) => {
208
+ const count = read(count$);
206
209
  return count === 0 ? "Click to start!" : `Count: ${count}`;
207
210
  });
208
211
 
209
212
  // Step 3: React to changes with effects
210
- effect(({ get }) => {
211
- console.log("Current count:", get(count$));
212
- console.log("Doubled value:", get(doubled$));
213
+ effect(({ read }) => {
214
+ console.log("Current count:", read(count$));
215
+ console.log("Doubled value:", read(doubled$));
213
216
  });
214
217
 
215
218
  // Step 4: Update state
@@ -221,7 +224,7 @@ count$.set((n) => n + 1); // Logs: Current count: 6, Doubled value: 12
221
224
 
222
225
  ```tsx
223
226
  import { atom, derived } from "atomirx";
224
- import { useValue, rx } from "atomirx/react";
227
+ import { useSelector, rx } from "atomirx/react";
225
228
 
226
229
  // Define your state
227
230
  interface Todo {
@@ -234,9 +237,9 @@ const todos$ = atom<Todo[]>([]);
234
237
  const filter$ = atom<"all" | "active" | "completed">("all");
235
238
 
236
239
  // Derive computed state
237
- const filteredTodos$ = derived(({ get }) => {
238
- const todos = get(todos$);
239
- const filter = get(filter$);
240
+ const filteredTodos$ = derived(({ read }) => {
241
+ const todos = read(todos$);
242
+ const filter = read(filter$);
240
243
 
241
244
  switch (filter) {
242
245
  case "active":
@@ -248,8 +251,8 @@ const filteredTodos$ = derived(({ get }) => {
248
251
  }
249
252
  });
250
253
 
251
- const stats$ = derived(({ get }) => {
252
- const todos = get(todos$);
254
+ const stats$ = derived(({ read }) => {
255
+ const todos = read(todos$);
253
256
  return {
254
257
  total: todos.length,
255
258
  completed: todos.filter((t) => t.completed).length,
@@ -270,7 +273,7 @@ const toggleTodo = (id: number) => {
270
273
 
271
274
  // Components
272
275
  function TodoList() {
273
- const todos = useValue(filteredTodos$);
276
+ const todos = useSelector(filteredTodos$);
274
277
 
275
278
  return (
276
279
  <ul>
@@ -287,8 +290,8 @@ function Stats() {
287
290
  // Fine-grained updates: only re-renders when stats change
288
291
  return (
289
292
  <footer>
290
- {rx(({ get }) => {
291
- const { total, completed, remaining } = get(stats$);
293
+ {rx(({ read }) => {
294
+ const { total, completed, remaining } = read(stats$);
292
295
  return (
293
296
  <span>
294
297
  {remaining} of {total} remaining
@@ -300,9 +303,9 @@ function Stats() {
300
303
  }
301
304
  ```
302
305
 
303
- ## Code Conventions
306
+ ## Patterns & Best Practices
304
307
 
305
- Following consistent conventions makes atomirx code more readable and maintainable across your team.
308
+ Following consistent patterns and best practices makes atomirx code more readable and maintainable across your team.
306
309
 
307
310
  ### Naming: The `$` Suffix
308
311
 
@@ -316,7 +319,7 @@ All atoms (both `atom()` and `derived()`) should use the `$` suffix. This conven
316
319
  // ✅ Good - clear that these are atoms
317
320
  const count$ = atom(0);
318
321
  const user$ = atom<User | null>(null);
319
- const filteredItems$ = derived(({ get }) => /* ... */);
322
+ const filteredItems$ = derived(({ read }) => /* ... */);
320
323
 
321
324
  // ❌ Avoid - unclear what's reactive
322
325
  const count = atom(0);
@@ -353,7 +356,7 @@ const todos$ = atom(() => fetchTodos());
353
356
  const filter$ = atom("all");
354
357
 
355
358
  function TodoList() {
356
- const todos = useValue(filteredTodos$);
359
+ const todos = useSelector(filteredTodos$);
357
360
  // ...
358
361
  }
359
362
  ```
@@ -367,9 +370,9 @@ const TodoModule = define(() => {
367
370
  const todos$ = atom(() => fetchTodos(), { meta: { key: "todos" } });
368
371
  const filter$ = atom<"all" | "active" | "completed">("all");
369
372
 
370
- const filteredTodos$ = derived(({ get }) => {
371
- const filter = get(filter$);
372
- const todos = get(todos$);
373
+ const filteredTodos$ = derived(({ read }) => {
374
+ const filter = read(filter$);
375
+ const todos = read(todos$);
373
376
  return filter === "all" ? todos : todos.filter(/* ... */);
374
377
  });
375
378
 
@@ -386,7 +389,7 @@ const TodoModule = define(() => {
386
389
  // Usage in React
387
390
  function TodoList() {
388
391
  const { filteredTodos$, setFilter } = TodoModule();
389
- const todos = useValue(filteredTodos$);
392
+ const todos = useSelector(filteredTodos$);
390
393
  // ...
391
394
  }
392
395
  ```
@@ -414,9 +417,9 @@ const userData$ = atom(fetchUser(userId)); // Fetch immediately
414
417
 
415
418
  ```typescript
416
419
  // derived() automatically unwraps Promises from atoms
417
- const filteredTodoList$ = derived(({ get }) => {
418
- const filter = get(filter$);
419
- const todoList = get(todoList$); // Promise is unwrapped automatically!
420
+ const filteredTodoList$ = derived(({ read }) => {
421
+ const filter = read(filter$);
422
+ const todoList = read(todoList$); // Promise is unwrapped automatically!
420
423
 
421
424
  switch (filter) {
422
425
  case "active":
@@ -433,8 +436,8 @@ const filteredTodoList$ = derived(({ get }) => {
433
436
 
434
437
  ```typescript
435
438
  // Sync local state to server when it changes
436
- effect(({ get, onCleanup }) => {
437
- const settings = get(settings$);
439
+ effect(({ read, onCleanup }) => {
440
+ const settings = read(settings$);
438
441
 
439
442
  const controller = new AbortController();
440
443
  saveSettingsToServer(settings, { signal: controller.signal });
@@ -443,8 +446,8 @@ effect(({ get, onCleanup }) => {
443
446
  });
444
447
 
445
448
  // Update multiple atoms based on another atom's change
446
- effect(({ get }) => {
447
- const user = get(currentUser$);
449
+ effect(({ read }) => {
450
+ const user = read(currentUser$);
448
451
 
449
452
  if (user) {
450
453
  // Trigger fetches for user-specific data
@@ -454,11 +457,204 @@ effect(({ get }) => {
454
457
  });
455
458
  ```
456
459
 
460
+ ### Error Handling: Use `safe()` Not try/catch
461
+
462
+ When working with reactive selectors in `derived()`, `effect()`, `useSelector()`, and `rx()`, you need to be careful about how you handle errors. The standard JavaScript `try/catch` pattern can break atomirx's Suspense mechanism.
463
+
464
+ #### The Problem with try/catch
465
+
466
+ The `read()` function in selectors uses a **Suspense-like pattern**: when an atom is loading (contains a pending Promise), `read()` throws that Promise. This is how atomirx signals to React's Suspense that it should show a fallback.
467
+
468
+ **If you wrap `read()` in a try/catch, you'll catch the Promise** along with any actual errors:
469
+
470
+ ```typescript
471
+ // ❌ WRONG - This breaks Suspense!
472
+ const data$ = derived(({ read }) => {
473
+ try {
474
+ const user = read(asyncUser$); // Throws Promise when loading!
475
+ return processUser(user);
476
+ } catch (e) {
477
+ // This catches BOTH:
478
+ // 1. The Promise (when loading) - breaks Suspense!
479
+ // 2. Actual errors from processUser()
480
+ return null;
481
+ }
482
+ });
483
+ ```
484
+
485
+ This causes several problems:
486
+
487
+ 1. **Loading state is lost** - Instead of suspending, your derived atom immediately returns `null`
488
+ 2. **No Suspense fallback** - React never sees the loading state
489
+ 3. **Silent failures** - You can't distinguish between "loading" and "error"
490
+
491
+ #### The Solution: safe()
492
+
493
+ atomirx provides the `safe()` utility in all selector contexts. It catches actual errors but **re-throws Promises** to preserve Suspense:
494
+
495
+ ```typescript
496
+ // ✅ CORRECT - Use safe() for error handling
497
+ const data$ = derived(({ read, safe }) => {
498
+ const [err, user] = safe(() => {
499
+ const raw = read(asyncUser$); // Can throw Promise (Suspense) ✓
500
+ return processUser(raw); // Can throw Error ✓
501
+ });
502
+
503
+ if (err) {
504
+ // Only actual errors reach here, not loading state
505
+ console.error("Processing failed:", err);
506
+ return { error: err.message };
507
+ }
508
+
509
+ return { user };
510
+ });
511
+ ```
512
+
513
+ **How `safe()` works:**
514
+
515
+ | Scenario | `try/catch` | `safe()` |
516
+ | ----------------- | ------------------ | ------------------------------- |
517
+ | Loading (Promise) | ❌ Catches Promise | ✅ Re-throws → Suspense |
518
+ | Error | ✅ Catches error | ✅ Returns `[error, undefined]` |
519
+ | Success | ✅ Returns value | ✅ Returns `[undefined, value]` |
520
+
521
+ #### Use Cases for safe()
522
+
523
+ **1. Parsing/Validation that might fail:**
524
+
525
+ ```typescript
526
+ const parsedConfig$ = derived(({ read, safe }) => {
527
+ const [err, config] = safe(() => {
528
+ const raw = read(rawConfig$);
529
+ return JSON.parse(raw); // Can throw SyntaxError
530
+ });
531
+
532
+ if (err) {
533
+ return { valid: false, error: "Invalid JSON" };
534
+ }
535
+ return { valid: true, config };
536
+ });
537
+ ```
538
+
539
+ **2. Graceful degradation with multiple sources:**
540
+
541
+ ```typescript
542
+ const dashboard$ = derived(({ read, safe }) => {
543
+ // Primary data - required
544
+ const user = read(user$);
545
+
546
+ // Optional data - graceful degradation
547
+ const [err1, analytics] = safe(() => read(analytics$));
548
+ const [err2, notifications] = safe(() => read(notifications$));
549
+
550
+ return {
551
+ user,
552
+ analytics: err1 ? null : analytics,
553
+ notifications: err2 ? [] : notifications,
554
+ errors: [err1, err2].filter(Boolean),
555
+ };
556
+ });
557
+ ```
558
+
559
+ **3. Error handling in effects:**
560
+
561
+ ```typescript
562
+ effect(({ read, safe }) => {
563
+ const [err, data] = safe(() => {
564
+ const raw = read(asyncData$);
565
+ return transformData(raw);
566
+ });
567
+
568
+ if (err) {
569
+ console.error("Effect failed:", err);
570
+ return; // Skip the rest of the effect
571
+ }
572
+
573
+ saveToLocalStorage(data);
574
+ });
575
+ ```
576
+
577
+ **4. Error handling in React components:**
578
+
579
+ ```tsx
580
+ function UserProfile() {
581
+ const result = useSelector(({ read, safe }) => {
582
+ const [err, user] = safe(() => read(user$));
583
+ return { err, user };
584
+ });
585
+
586
+ if (result.err) {
587
+ return <ErrorMessage error={result.err} />;
588
+ }
589
+
590
+ return <Profile user={result.user} />;
591
+ }
592
+ ```
593
+
594
+ **5. With rx() for inline error handling:**
595
+
596
+ ```tsx
597
+ <Suspense fallback={<Loading />}>
598
+ {rx(({ read, safe }) => {
599
+ const [err, posts] = safe(() => read(posts$));
600
+ if (err) return <ErrorBanner message="Failed to load posts" />;
601
+ return posts.map((post) => <PostCard key={post.id} post={post} />);
602
+ })}
603
+ </Suspense>
604
+ ```
605
+
606
+ ### SelectContext Methods: Synchronous Only
607
+
608
+ All context methods (`read`, `all`, `race`, `any`, `settled`, `safe`) must be called **synchronously** during selector execution. They cannot be used in async callbacks like `setTimeout`, `Promise.then`, or event handlers.
609
+
610
+ ```typescript
611
+ // ❌ WRONG - Calling read() in async callback
612
+ derived(({ read }) => {
613
+ setTimeout(() => {
614
+ read(atom$); // Error: called outside selection context
615
+ }, 100);
616
+ return "value";
617
+ });
618
+
619
+ // ❌ WRONG - Storing read() for later use
620
+ let savedRead;
621
+ select(({ read }) => {
622
+ savedRead = read; // Don't do this!
623
+ return read(atom$);
624
+ });
625
+ savedRead(atom$); // Error: called outside selection context
626
+
627
+ // ✅ CORRECT - For async access, use atom.get() directly
628
+ effect(({ read }) => {
629
+ const config = read(config$);
630
+
631
+ setTimeout(async () => {
632
+ // Use atom.get() for async access (not tracked as dependency)
633
+ const data = myMutableAtom$.get();
634
+ const asyncData = await myDerivedAtom$.get();
635
+ console.log(data, asyncData);
636
+ }, 100);
637
+ });
638
+ ```
639
+
640
+ **Why this restriction?**
641
+
642
+ 1. **Dependency tracking**: Context methods track which atoms are accessed to know when to recompute. This tracking only works during synchronous execution.
643
+
644
+ 2. **Predictable behavior**: If `read()` could be called at any time, the reactive graph would be unpredictable and hard to debug.
645
+
646
+ 3. **Clear error messages**: Rather than silently failing to track dependencies, atomirx throws a helpful error explaining the issue.
647
+
648
+ **For async access**, use `atom.get()` directly:
649
+
650
+ - `mutableAtom$.get()` - Returns the raw value (may be a Promise)
651
+ - `await derivedAtom$.get()` - Returns a Promise that resolves to the computed value
652
+
457
653
  ### Complete Example: Todo App with Async
458
654
 
459
655
  ```typescript
460
656
  import { atom, derived } from "atomirx";
461
- import { useValue, rx } from "atomirx/react";
657
+ import { useSelector, rx } from "atomirx/react";
462
658
  import { Suspense } from "react";
463
659
 
464
660
  // Atoms store values (including Promises)
@@ -466,9 +662,9 @@ const filter$ = atom<"all" | "active" | "completed">("all");
466
662
  const todoList$ = atom(() => fetchAllTodos()); // Lazy init, re-runs on reset()
467
663
 
468
664
  // Derived handles reactive transformations (auto-unwraps Promises)
469
- const filteredTodoList$ = derived(({ get }) => {
470
- const filter = get(filter$);
471
- const todoList = get(todoList$); // This is the resolved value, not a Promise!
665
+ const filteredTodoList$ = derived(({ read }) => {
666
+ const filter = read(filter$);
667
+ const todoList = read(todoList$); // This is the resolved value, not a Promise!
472
668
 
473
669
  switch (filter) {
474
670
  case "active": return todoList.filter(t => !t.completed);
@@ -477,9 +673,9 @@ const filteredTodoList$ = derived(({ get }) => {
477
673
  }
478
674
  });
479
675
 
480
- // In UI - useValue suspends until data is ready
676
+ // In UI - useSelector suspends until data is ready
481
677
  function TodoList() {
482
- const filteredTodoList = useValue(filteredTodoList$);
678
+ const filteredTodoList = useSelector(filteredTodoList$);
483
679
 
484
680
  return (
485
681
  <ul>
@@ -494,8 +690,8 @@ function TodoList() {
494
690
  function App() {
495
691
  return (
496
692
  <Suspense fallback={<div>Loading todos...</div>}>
497
- {rx(({ get }) =>
498
- get(filteredTodoList$).map(todo => <Todo key={todo.id} todo={todo} />)
693
+ {rx(({ read }) =>
694
+ read(filteredTodoList$).map(todo => <Todo key={todo.id} todo={todo} />)
499
695
  )}
500
696
  </Suspense>
501
697
  );
@@ -511,6 +707,229 @@ function RefreshButton() {
511
707
  }
512
708
  ```
513
709
 
710
+ ### Deferred Entity Loading with `ready()`
711
+
712
+ When building detail pages (e.g., `/article/:id`), you often need to:
713
+
714
+ 1. Wait for a route parameter to be set
715
+ 2. Fetch data based on that parameter
716
+ 3. Share the loaded entity across multiple components
717
+
718
+ The `ready()` method in derived atoms provides an elegant solution for this pattern.
719
+
720
+ #### The Pattern
721
+
722
+ ```typescript
723
+ import { atom, derived, effect, readonly, define } from "atomirx";
724
+
725
+ const articleModule = define(() => {
726
+ // Current article ID - set from route
727
+ const currentArticleId$ = atom<string | undefined>(undefined);
728
+
729
+ // Article cache - normalized storage
730
+ const articleCache$ = atom<Record<string, Article>>({});
731
+
732
+ // Current article - uses ready() to wait for both ID and cached data
733
+ const currentArticle$ = derived(({ ready }) => {
734
+ const id = ready(currentArticleId$); // Suspends if undefined
735
+ const article = ready(articleCache$, (cache) => cache[id]); // Suspends if not cached
736
+ return article;
737
+ });
738
+
739
+ // Fetch article when ID changes
740
+ effect(({ read }) => {
741
+ const id = read(currentArticleId$);
742
+ if (!id) return;
743
+
744
+ // Skip if already cached
745
+ const cache = read(articleCache$);
746
+ if (cache[id]) return;
747
+
748
+ // Fetch and cache
749
+ // ─────────────────────────────────────────────────────────────
750
+ // Optional: Track loading/error states in cache for more control
751
+ // type CacheEntry<T> =
752
+ // | { status: "loading" }
753
+ // | { status: "error"; error: Error }
754
+ // | { status: "success"; data: T };
755
+ // const articleCache$ = atom<Record<string, CacheEntry<Article>>>({});
756
+ //
757
+ // articleCache$.set((prev) => ({ ...prev, [id]: { status: "loading" } }));
758
+ // fetch(...)
759
+ // .then((article) => articleCache$.set((prev) => ({
760
+ // ...prev, [id]: { status: "success", data: article }
761
+ // })))
762
+ // .catch((error) => articleCache$.set((prev) => ({
763
+ // ...prev, [id]: { status: "error", error }
764
+ // })));
765
+ // ─────────────────────────────────────────────────────────────
766
+ fetch(`/api/articles/${id}`)
767
+ .then((r) => r.json())
768
+ .then((article) => {
769
+ articleCache$.set((prev) => ({ ...prev, [id]: article }));
770
+ });
771
+ });
772
+
773
+ return {
774
+ ...readonly({ currentArticleId$, currentArticle$ }),
775
+
776
+ // ─────────────────────────────────────────────────────────────
777
+ // navigateTo(id) - Navigate to a new article
778
+ // ─────────────────────────────────────────────────────────────
779
+ // Flow:
780
+ // 1. Set currentArticleId$ to new ID
781
+ // 2. currentArticle$ recomputes:
782
+ // - If cached: ready() succeeds → UI shows article immediately
783
+ // - If not cached: ready() suspends → UI shows <Skeleton />
784
+ // 3. effect() runs in parallel:
785
+ // - If cached: early return (no fetch)
786
+ // - If not cached: fetch → update articleCache$
787
+ // 4. When cache updated, currentArticle$ recomputes → UI shows article
788
+ navigateTo: (id: string) => currentArticleId$.set(id),
789
+
790
+ // ─────────────────────────────────────────────────────────────
791
+ // invalidate(id) - Mark an article as stale (soft invalidation)
792
+ // ─────────────────────────────────────────────────────────────
793
+ // Flow:
794
+ // 1. Guard: Skip if id === currentArticleId (don't disrupt current view)
795
+ // 2. Remove article from cache
796
+ // 3. No immediate effect on UI (current article unchanged)
797
+ // 4. Next navigateTo(id) will trigger fresh fetch
798
+ // Use case: Background sync detected article was updated elsewhere
799
+ invalidate: (id: string) => {
800
+ if (id === currentArticleId$.get()) return;
801
+ articleCache$.set((prev) => {
802
+ const { [id]: _, ...rest } = prev;
803
+ return rest;
804
+ });
805
+ },
806
+
807
+ // ─────────────────────────────────────────────────────────────
808
+ // refresh() - Force refetch current article (hard refresh)
809
+ // ─────────────────────────────────────────────────────────────
810
+ // Flow:
811
+ // 1. Remove current article from cache
812
+ // 2. currentArticle$ recomputes:
813
+ // - ready() suspends (cache miss) → UI shows <Skeleton />
814
+ // 3. effect() runs:
815
+ // - Cache miss detected → fetch starts
816
+ // 4. Fetch completes → cache updated → UI shows fresh data
817
+ // Use case: Pull-to-refresh, retry after error
818
+ refresh() {
819
+ articleCache$.set((prev) => {
820
+ const { [currentArticleId$.get()]: _, ...rest } = prev;
821
+ return rest;
822
+ });
823
+ },
824
+ };
825
+ });
826
+ ```
827
+
828
+ #### How It Works
829
+
830
+ ```
831
+ ┌─────────────────────────────────────────────────────────────┐
832
+ │ Route: /article/:id │
833
+ │ → navigateTo(id) │
834
+ └─────────────────────────────────────────────────────────────┘
835
+
836
+
837
+ ┌─────────────────────────────────────────────────────────────┐
838
+ │ effect() detects ID change │
839
+ │ → Checks cache, fetches if not cached │
840
+ │ → Updates articleCache$ │
841
+ └─────────────────────────────────────────────────────────────┘
842
+
843
+
844
+ ┌─────────────────────────────────────────────────────────────┐
845
+ │ currentArticle$ (derived with ready()) │
846
+ │ → Suspends until ID is set AND article is in cache │
847
+ │ → Returns article when both conditions met │
848
+ └─────────────────────────────────────────────────────────────┘
849
+
850
+
851
+ ┌─────────────────────────────────────────────────────────────┐
852
+ │ Components with Suspense │
853
+ │ → Show loading fallback while suspended │
854
+ │ → Render article when ready │
855
+ └─────────────────────────────────────────────────────────────┘
856
+ ```
857
+
858
+ #### Component Usage
859
+
860
+ ```tsx
861
+ // Page component - syncs route and provides boundaries
862
+ export const ArticlePage = () => {
863
+ const { id } = useParams();
864
+ const { navigateTo } = articleModule();
865
+
866
+ useEffect(() => {
867
+ navigateTo(id);
868
+ }, [id]);
869
+
870
+ return (
871
+ <ErrorBoundary fallback={<ErrorDialog />}>
872
+ <Suspense fallback={<ArticleSkeleton />}>
873
+ <ArticleHeader />
874
+ <ArticleContent />
875
+ <ArticleMeta />
876
+ </Suspense>
877
+ </ErrorBoundary>
878
+ );
879
+ };
880
+
881
+ // Child components - clean, no loading/error handling needed
882
+ export const ArticleHeader = () => {
883
+ const { currentArticle$ } = articleModule();
884
+ const article = useSelector(currentArticle$);
885
+
886
+ return <h1>{article.title}</h1>;
887
+ };
888
+
889
+ export const ArticleContent = () => {
890
+ const { currentArticle$ } = articleModule();
891
+ const article = useSelector(currentArticle$);
892
+
893
+ return <div className="content">{article.body}</div>;
894
+ };
895
+
896
+ export const ArticleMeta = () => {
897
+ const { currentArticle$ } = articleModule();
898
+ const article = useSelector(currentArticle$);
899
+
900
+ return (
901
+ <span>
902
+ By {article.author} • {article.date}
903
+ </span>
904
+ );
905
+ };
906
+ ```
907
+
908
+ #### Benefits
909
+
910
+ | Benefit | Description |
911
+ | ------------------------ | ---------------------------------------------------------------- |
912
+ | **Clean components** | Child components just read the atom - no loading/error handling |
913
+ | **No prop drilling** | All components access `currentArticle$` directly from the module |
914
+ | **Automatic suspension** | `ready()` handles the "wait for data" logic declaratively |
915
+ | **Centralized fetching** | Effect handles when/how to fetch, components just consume |
916
+ | **Cache management** | Normalized cache enables invalidation and updates |
917
+
918
+ #### When to Use `ready()`
919
+
920
+ | Use Case | Example |
921
+ | -------------------- | ------------------------------------------------ |
922
+ | Route-based entities | `/article/:id`, `/user/:userId`, `/product/:sku` |
923
+ | Auth-gated content | Wait for `currentUser$` before showing dashboard |
924
+ | Dependent data | Wait for parent entity before fetching children |
925
+ | Multi-step forms | Wait for previous step data before showing next |
926
+
927
+ #### Important Notes
928
+
929
+ - **Only use in `derived()` or `effect()`** - `ready()` suspends computation, which only works in reactive contexts
930
+ - **Separate fetching from derivation** - Use `effect()` for side effects (fetching), `derived()` for computing values
931
+ - **Read from cache in effect** - Don't read `currentArticle$` in the effect that populates it; read `articleCache$` directly
932
+
514
933
  ## Usage Guide
515
934
 
516
935
  ### Atoms: The Foundation
@@ -543,7 +962,7 @@ const posts$ = atom(fetchPosts(), { fallback: [] });
543
962
  import { getAtomState, isPending } from "atomirx";
544
963
 
545
964
  // Direct access (outside reactive context)
546
- console.log(count$.value); // Current value (T or Promise<T>)
965
+ console.log(count$.get()); // Current value (T or Promise<T>)
547
966
 
548
967
  // Check atom state
549
968
  const state = getAtomState(userData$);
@@ -556,7 +975,7 @@ if (state.status === "loading") {
556
975
  }
557
976
 
558
977
  // Quick loading check
559
- console.log(isPending(userData$.value)); // true while Promise is pending
978
+ console.log(isPending(userData$.get())); // true while Promise is pending
560
979
  ```
561
980
 
562
981
  #### Updating Atoms
@@ -585,7 +1004,7 @@ const unsubscribe = count$.on((newValue) => {
585
1004
 
586
1005
  // Await async atoms
587
1006
  await userData$;
588
- console.log("User loaded:", userData$.value);
1007
+ console.log("User loaded:", userData$.get());
589
1008
 
590
1009
  // Unsubscribe when done
591
1010
  unsubscribe();
@@ -597,7 +1016,7 @@ unsubscribe();
597
1016
 
598
1017
  | Property/Method | Type | Description |
599
1018
  | --------------- | ------------ | -------------------------------------------------- |
600
- | `value` | `T` | Current value (may be a Promise for async atoms) |
1019
+ | `get()` | `T` | Current value (may be a Promise for async atoms) |
601
1020
  | `set(value)` | `void` | Update with value, Promise, or updater function |
602
1021
  | `reset()` | `void` | Reset to initial value |
603
1022
  | `on(listener)` | `() => void` | Subscribe to changes, returns unsubscribe function |
@@ -606,7 +1025,7 @@ unsubscribe();
606
1025
 
607
1026
  | Property/Method | Type | Description |
608
1027
  | --------------- | ---------------- | ---------------------------------------------- |
609
- | `value` | `Promise<T>` | Always returns a Promise |
1028
+ | `get()` | `Promise<T>` | Always returns a Promise |
610
1029
  | `staleValue` | `T \| undefined` | Fallback or last resolved value during loading |
611
1030
  | `state()` | `AtomState<T>` | Current state (ready/error/loading) |
612
1031
  | `refresh()` | `void` | Re-run the computation |
@@ -625,6 +1044,8 @@ type AtomState<T> =
625
1044
 
626
1045
  Derived atoms automatically compute values based on other atoms. They track dependencies at runtime and only recompute when those specific dependencies change.
627
1046
 
1047
+ > **Note:** For error handling in derived selectors, use `safe()` instead of try/catch. See [Error Handling: Use `safe()` Not try/catch](#error-handling-use-safe-not-trycatch).
1048
+
628
1049
  #### Basic Derived State
629
1050
 
630
1051
  ```typescript
@@ -634,12 +1055,12 @@ const firstName$ = atom("John");
634
1055
  const lastName$ = atom("Doe");
635
1056
 
636
1057
  // Derived state with automatic dependency tracking
637
- const fullName$ = derived(({ get }) => {
638
- return `${get(firstName$)} ${get(lastName$)}`;
1058
+ const fullName$ = derived(({ read }) => {
1059
+ return `${read(firstName$)} ${read(lastName$)}`;
639
1060
  });
640
1061
 
641
- // Derived atoms always return Promise<T> for .value
642
- await fullName$.value; // "John Doe"
1062
+ // Derived atoms always return Promise<T> for .get()
1063
+ await fullName$.get(); // "John Doe"
643
1064
 
644
1065
  // Or use staleValue for synchronous access (after first resolution)
645
1066
  fullName$.staleValue; // "John Doe" (or undefined before first resolution)
@@ -648,7 +1069,7 @@ fullName$.staleValue; // "John Doe" (or undefined before first resolution)
648
1069
  fullName$.state(); // { status: "ready", value: "John Doe" }
649
1070
 
650
1071
  firstName$.set("Jane");
651
- await fullName$.value; // "Jane Doe"
1072
+ await fullName$.get(); // "Jane Doe"
652
1073
  ```
653
1074
 
654
1075
  #### Conditional Dependencies
@@ -660,13 +1081,13 @@ const showDetails$ = atom(false);
660
1081
  const summary$ = atom("Brief overview");
661
1082
  const details$ = atom("Detailed information...");
662
1083
 
663
- const content$ = derived(({ get }) => {
1084
+ const content$ = derived(({ read }) => {
664
1085
  // Only tracks showDetails$ initially
665
- if (get(showDetails$)) {
1086
+ if (read(showDetails$)) {
666
1087
  // details$ becomes a dependency only when showDetails$ is true
667
- return get(details$);
1088
+ return read(details$);
668
1089
  }
669
- return get(summary$);
1090
+ return read(summary$);
670
1091
  });
671
1092
 
672
1093
  // When showDetails$ is false:
@@ -676,9 +1097,9 @@ const content$ = derived(({ get }) => {
676
1097
 
677
1098
  #### Suspense-Style Getters
678
1099
 
679
- The `get()` function follows React Suspense semantics for async atoms:
1100
+ The `read()` function follows React Suspense semantics for async atoms:
680
1101
 
681
- | Atom State | `get()` Behavior |
1102
+ | Atom State | `read()` Behavior |
682
1103
  | ---------- | ----------------------------------------------- |
683
1104
  | Loading | Throws the Promise (caught by derived/Suspense) |
684
1105
  | Error | Throws the error |
@@ -687,9 +1108,9 @@ The `get()` function follows React Suspense semantics for async atoms:
687
1108
  ```typescript
688
1109
  const user$ = atom(fetchUser());
689
1110
 
690
- const userName$ = derived(({ get }) => {
1111
+ const userName$ = derived(({ read }) => {
691
1112
  // Automatically handles loading/error states
692
- const user = get(user$);
1113
+ const user = read(user$);
693
1114
  return user.name;
694
1115
  });
695
1116
 
@@ -704,7 +1125,7 @@ if (state.status === "loading") {
704
1125
  }
705
1126
 
706
1127
  // Or use staleValue with a fallback
707
- const userName = derived(({ get }) => get(user$).name, { fallback: "Guest" });
1128
+ const userName = derived(({ read }) => read(user$).name, { fallback: "Guest" });
708
1129
  userName.staleValue; // "Guest" during loading, then actual name
709
1130
  ```
710
1131
 
@@ -714,9 +1135,9 @@ userName.staleValue; // "Guest" during loading, then actual name
714
1135
  const user$ = atom(fetchUser());
715
1136
  const posts$ = atom(fetchPosts());
716
1137
 
717
- const dashboard$ = derived(({ get }) => {
718
- const user = get(user$); // Suspends if loading
719
- const posts = get(posts$); // Suspends if loading
1138
+ const dashboard$ = derived(({ read }) => {
1139
+ const user = read(user$); // Suspends if loading
1140
+ const posts = read(posts$); // Suspends if loading
720
1141
 
721
1142
  return {
722
1143
  userName: user.name,
@@ -730,7 +1151,7 @@ const state = dashboard$.state();
730
1151
 
731
1152
  // Or use all() for explicit parallel loading
732
1153
  const dashboard2$ = derived(({ all }) => {
733
- const [user, posts] = all(user$, posts$);
1154
+ const [user, posts] = all([user$, posts$]);
734
1155
  return { userName: user.name, postCount: posts.length };
735
1156
  });
736
1157
  ```
@@ -739,6 +1160,8 @@ const dashboard2$ = derived(({ all }) => {
739
1160
 
740
1161
  Effects run side effects whenever their dependencies change. They use the same reactive context as `derived()`.
741
1162
 
1163
+ > **Note:** For error handling in effects, use `safe()` instead of try/catch. See [Error Handling: Use `safe()` Not try/catch](#error-handling-use-safe-not-trycatch).
1164
+
742
1165
  #### Basic Effects
743
1166
 
744
1167
  ```typescript
@@ -747,8 +1170,8 @@ import { atom, effect } from "atomirx";
747
1170
  const count$ = atom(0);
748
1171
 
749
1172
  // Effect runs immediately and on every change
750
- const dispose = effect(({ get }) => {
751
- console.log("Count is now:", get(count$));
1173
+ const dispose = effect(({ read }) => {
1174
+ console.log("Count is now:", read(count$));
752
1175
  });
753
1176
 
754
1177
  count$.set(5); // Logs: "Count is now: 5"
@@ -764,8 +1187,8 @@ Use `onCleanup()` to register cleanup functions that run before the next executi
764
1187
  ```typescript
765
1188
  const interval$ = atom(1000);
766
1189
 
767
- const dispose = effect(({ get, onCleanup }) => {
768
- const ms = get(interval$);
1190
+ const dispose = effect(({ read, onCleanup }) => {
1191
+ const ms = read(interval$);
769
1192
  const id = setInterval(() => console.log("tick"), ms);
770
1193
 
771
1194
  // Cleanup runs before next execution or on dispose
@@ -776,36 +1199,15 @@ interval$.set(500); // Clears old interval, starts new one
776
1199
  dispose(); // Clears interval completely
777
1200
  ```
778
1201
 
779
- #### Effects with Error Handling
780
-
781
- Use `onError()` to handle errors within the effect:
782
-
783
- ```typescript
784
- const dispose = effect(({ get, onError }) => {
785
- onError((e) => console.error("Effect failed:", e));
786
-
787
- const data = get(dataAtom$);
788
- riskyOperation(data);
789
- });
790
-
791
- // Or use options.onError for unhandled errors
792
- const dispose2 = effect(
793
- ({ get }) => {
794
- riskyOperation(get(dataAtom$));
795
- },
796
- { onError: (e) => console.error("Unhandled:", e) }
797
- );
798
- ```
799
-
800
1202
  #### Effects with Multiple Dependencies
801
1203
 
802
1204
  ```typescript
803
1205
  const user$ = atom<User | null>(null);
804
1206
  const settings$ = atom({ notifications: true });
805
1207
 
806
- effect(({ get }) => {
807
- const user = get(user$);
808
- const settings = get(settings$);
1208
+ effect(({ read }) => {
1209
+ const user = read(user$);
1210
+ const settings = read(settings$);
809
1211
 
810
1212
  if (user && settings.notifications) {
811
1213
  analytics.identify(user.id);
@@ -826,8 +1228,8 @@ const posts$ = atom(fetchPosts());
826
1228
  const comments$ = atom(fetchComments());
827
1229
 
828
1230
  const dashboard$ = derived(({ all }) => {
829
- // Suspends until ALL atoms resolve (variadic args)
830
- const [user, posts, comments] = all(user$, posts$, comments$);
1231
+ // Suspends until ALL atoms resolve (array-based)
1232
+ const [user, posts, comments] = all([user$, posts$, comments$]);
831
1233
 
832
1234
  return { user, posts, comments };
833
1235
  });
@@ -840,8 +1242,9 @@ const primaryApi$ = atom(fetchFromPrimary());
840
1242
  const fallbackApi$ = atom(fetchFromFallback());
841
1243
 
842
1244
  const data$ = derived(({ any }) => {
843
- // Returns first successfully resolved value (variadic args)
844
- return any(primaryApi$, fallbackApi$);
1245
+ // Returns first successfully resolved value (object-based, returns { key, value })
1246
+ const result = any({ primary: primaryApi$, fallback: fallbackApi$ });
1247
+ return result.value;
845
1248
  });
846
1249
  ```
847
1250
 
@@ -852,8 +1255,9 @@ const cache$ = atom(checkCache());
852
1255
  const api$ = atom(fetchFromApi());
853
1256
 
854
1257
  const data$ = derived(({ race }) => {
855
- // Returns first settled (ready OR error) (variadic args)
856
- return race(cache$, api$);
1258
+ // Returns first settled (ready OR error) (object-based, returns { key, value })
1259
+ const result = race({ cache: cache$, api: api$ });
1260
+ return result.value;
857
1261
  });
858
1262
  ```
859
1263
 
@@ -864,8 +1268,8 @@ const user$ = atom(fetchUser());
864
1268
  const posts$ = atom(fetchPosts());
865
1269
 
866
1270
  const results$ = derived(({ settled }) => {
867
- // Returns status for each atom (variadic args)
868
- const [userResult, postsResult] = settled(user$, posts$);
1271
+ // Returns status for each atom (array-based)
1272
+ const [userResult, postsResult] = settled([user$, posts$]);
869
1273
 
870
1274
  return {
871
1275
  user: userResult.status === "ready" ? userResult.value : null,
@@ -877,12 +1281,12 @@ const results$ = derived(({ settled }) => {
877
1281
 
878
1282
  #### Async Utility Summary
879
1283
 
880
- | Utility | Input | Output | Behavior |
881
- | ----------- | -------------- | ---------------------- | ---------------------------------- |
882
- | `all()` | Variadic atoms | Array of values | Suspends until all ready |
883
- | `any()` | Variadic atoms | First ready value | First to resolve wins |
884
- | `race()` | Variadic atoms | First settled value | First to settle (ready/error) wins |
885
- | `settled()` | Variadic atoms | Array of SettledResult | Suspends until all settled |
1284
+ | Utility | Input | Output | Behavior |
1285
+ | ----------- | --------------- | ------------------------ | ---------------------------------- |
1286
+ | `all()` | Array of atoms | Array of values | Suspends until all ready |
1287
+ | `any()` | Record of atoms | `{ key, value }` (first) | First to resolve wins |
1288
+ | `race()` | Record of atoms | `{ key, value }` (first) | First to settle (ready/error) wins |
1289
+ | `settled()` | Array of atoms | Array of SettledResult | Suspends until all settled |
886
1290
 
887
1291
  **SettledResult type:**
888
1292
 
@@ -1039,7 +1443,7 @@ onCreateHook.override((prev) => (info) => {
1039
1443
 
1040
1444
  // Save to localStorage on every change
1041
1445
  info.atom.on(() => {
1042
- localStorage.setItem(storageKey, JSON.stringify(info.atom.value));
1446
+ localStorage.setItem(storageKey, JSON.stringify(info.atom.get()));
1043
1447
  });
1044
1448
  }
1045
1449
  });
@@ -1088,7 +1492,7 @@ onCreateHook.override((prev) => (info) => {
1088
1492
  // Resolve the next value (handle both direct value and reducer)
1089
1493
  const nextValue =
1090
1494
  typeof valueOrReducer === "function"
1091
- ? (valueOrReducer as (prev: unknown) => unknown)(info.atom.value)
1495
+ ? (valueOrReducer as (prev: unknown) => unknown)(info.atom.get())
1092
1496
  : valueOrReducer;
1093
1497
 
1094
1498
  // Validate before applying
@@ -1167,12 +1571,14 @@ interface ModuleCreateInfo {
1167
1571
 
1168
1572
  atomirx provides first-class React integration through the `atomirx/react` package.
1169
1573
 
1170
- ### useValue Hook
1574
+ ### useSelector Hook
1171
1575
 
1172
- Subscribe to atom values with automatic re-rendering:
1576
+ Subscribe to atom values with automatic re-rendering.
1577
+
1578
+ > **Note:** For error handling in selectors, use `safe()` instead of try/catch. See [Error Handling: Use `safe()` Not try/catch](#error-handling-use-safe-not-trycatch).
1173
1579
 
1174
1580
  ```tsx
1175
- import { useValue } from "atomirx/react";
1581
+ import { useSelector } from "atomirx/react";
1176
1582
  import { atom } from "atomirx";
1177
1583
 
1178
1584
  const count$ = atom(0);
@@ -1180,15 +1586,15 @@ const user$ = atom<User | null>(null);
1180
1586
 
1181
1587
  function Counter() {
1182
1588
  // Shorthand: pass atom directly
1183
- const count = useValue(count$);
1589
+ const count = useSelector(count$);
1184
1590
 
1185
1591
  // Context selector: compute derived value
1186
- const doubled = useValue(({ get }) => get(count$) * 2);
1592
+ const doubled = useSelector(({ read }) => read(count$) * 2);
1187
1593
 
1188
1594
  // Multiple atoms
1189
- const display = useValue(({ get }) => {
1190
- const count = get(count$);
1191
- const user = get(user$);
1595
+ const display = useSelector(({ read }) => {
1596
+ const count = read(count$);
1597
+ const user = read(user$);
1192
1598
  return user ? `${user.name}: ${count}` : `Anonymous: ${count}`;
1193
1599
  });
1194
1600
 
@@ -1200,15 +1606,100 @@ function Counter() {
1200
1606
 
1201
1607
  ```tsx
1202
1608
  // Only re-render when specific fields change
1203
- const userName = useValue(
1204
- ({ get }) => get(user$)?.name,
1609
+ const userName = useSelector(
1610
+ ({ read }) => read(user$)?.name,
1205
1611
  (prev, next) => prev === next
1206
1612
  );
1207
1613
  ```
1208
1614
 
1615
+ #### Why useSelector is Powerful
1616
+
1617
+ `useSelector` provides a unified API that replaces multiple hooks from other libraries:
1618
+
1619
+ **One hook for all use cases:**
1620
+
1621
+ ```tsx
1622
+ // 1. Single atom (shorthand)
1623
+ const count = useSelector(count$);
1624
+
1625
+ // 2. Derived value (selector)
1626
+ const doubled = useSelector(({ read }) => read(count$) * 2);
1627
+
1628
+ // 3. Multiple atoms (all) - array-based
1629
+ const [user, posts] = useSelector(({ all }) => all([user$, posts$]));
1630
+
1631
+ // 4. Loadable mode (state) - no Suspense needed
1632
+ const userState = useSelector(({ state }) => state(user$));
1633
+ // { status: "loading" | "ready" | "error", value, error }
1634
+
1635
+ // 5. Error handling (safe) - preserves Suspense
1636
+ const result = useSelector(({ read, safe }) => {
1637
+ const [err, data] = safe(() => JSON.parse(read(rawJson$)));
1638
+ return err ? { error: err.message } : { data };
1639
+ });
1640
+
1641
+ // 6. First ready (any), race, allSettled
1642
+ const fastest = useSelector(({ any }) => any({ cache: cache$, api: api$ }));
1643
+ const results = useSelector(({ settled }) => settled([a$, b$, c$]));
1644
+ ```
1645
+
1646
+ **Comparison with other libraries:**
1647
+
1648
+ | Use Case | atomirx | Jotai | Recoil | Zustand |
1649
+ | ------------------- | ------------------------------------------ | ------------------------------ | ------------------------------- | -------------------------- |
1650
+ | Single atom | `useSelector(atom$)` | `useAtomValue(atom)` | `useRecoilValue(atom)` | `useStore(s => s.value)` |
1651
+ | Derived value | `useSelector(({ read }) => ...)` | `useAtomValue(derivedAtom)` | `useRecoilValue(selector)` | `useStore(s => derive(s))` |
1652
+ | Multiple atoms | `useSelector(({ all }) => all([a$, b$]))` | Multiple `useAtomValue` calls | Multiple `useRecoilValue` calls | Multiple selectors |
1653
+ | Suspense mode | Built-in (default) | Built-in | Built-in | Manual |
1654
+ | Loadable mode | `useSelector(({ state }) => state(atom$))` | `useAtomValue(loadable(atom))` | `useRecoilValueLoadable(atom)` | Manual |
1655
+ | Safe error handling | `safe()` in selector | Manual try/catch | Manual try/catch | Manual |
1656
+ | Custom equality | 2nd parameter | `selectAtom` | N/A | 2nd parameter |
1657
+
1658
+ **Key advantages:**
1659
+
1660
+ 1. **Single unified hook** - No need to choose between `useAtomValue`, `useAtomValueLoadable`, etc.
1661
+ 2. **Composable selectors** - Combine multiple atoms, derive values, handle errors in one selector
1662
+ 3. **Flexible async modes** - Switch between Suspense and loadable without changing atoms
1663
+ 4. **Built-in utilities** - `all()`, `any()`, `race()`, `settled()`, `safe()`, `state()` available in selector
1664
+ 5. **Type-safe** - Full TypeScript inference across all patterns
1665
+
1666
+ **Boilerplate comparison - Loading multiple async atoms:**
1667
+
1668
+ ```tsx
1669
+ // ❌ Jotai - Multiple hooks + loadable wrapper
1670
+ const userLoadable = useAtomValue(loadable(userAtom));
1671
+ const postsLoadable = useAtomValue(loadable(postsAtom));
1672
+ const isLoading =
1673
+ userLoadable.state === "loading" || postsLoadable.state === "loading";
1674
+ const user = userLoadable.state === "hasData" ? userLoadable.data : null;
1675
+ const posts = postsLoadable.state === "hasData" ? postsLoadable.data : [];
1676
+
1677
+ // ❌ Recoil - Multiple hooks
1678
+ const userLoadable = useRecoilValueLoadable(userAtom);
1679
+ const postsLoadable = useRecoilValueLoadable(postsAtom);
1680
+ const isLoading =
1681
+ userLoadable.state === "loading" || postsLoadable.state === "loading";
1682
+ const user = userLoadable.state === "hasValue" ? userLoadable.contents : null;
1683
+ const posts = postsLoadable.state === "hasValue" ? postsLoadable.contents : [];
1684
+
1685
+ // ✅ atomirx - One hook, one selector
1686
+ const { isLoading, user, posts } = useSelector(({ state }) => {
1687
+ const userState = state(user$);
1688
+ const postsState = state(posts$);
1689
+ return {
1690
+ isLoading:
1691
+ userState.status === "loading" || postsState.status === "loading",
1692
+ user: userState.value,
1693
+ posts: postsState.value ?? [],
1694
+ };
1695
+ });
1696
+ ```
1697
+
1209
1698
  ### Reactive Components with rx
1210
1699
 
1211
- The `rx()` function creates inline reactive components for fine-grained updates:
1700
+ The `rx()` function creates inline reactive components for fine-grained updates.
1701
+
1702
+ > **Note:** For error handling in selectors, use `safe()` instead of try/catch. See [Error Handling: Use `safe()` Not try/catch](#error-handling-use-safe-not-trycatch).
1212
1703
 
1213
1704
  ```tsx
1214
1705
  import { rx } from "atomirx/react";
@@ -1220,17 +1711,17 @@ function Dashboard() {
1220
1711
  <span>Count: {rx(count$)}</span>
1221
1712
 
1222
1713
  {/* Derived value */}
1223
- <span>Doubled: {rx(({ get }) => get(count$) * 2)}</span>
1714
+ <span>Doubled: {rx(({ read }) => read(count$) * 2)}</span>
1224
1715
 
1225
1716
  {/* Complex rendering */}
1226
- {rx(({ get }) => {
1227
- const user = get(user$);
1717
+ {rx(({ read }) => {
1718
+ const user = read(user$);
1228
1719
  return user ? <UserCard user={user} /> : <LoginPrompt />;
1229
1720
  })}
1230
1721
 
1231
1722
  {/* Async with utilities */}
1232
1723
  {rx(({ all }) => {
1233
- const [user, posts] = all(user$, posts$);
1724
+ const [user, posts] = all([user$, posts$]);
1234
1725
  return <Feed user={user} posts={posts} />;
1235
1726
  })}
1236
1727
  </div>
@@ -1240,6 +1731,79 @@ function Dashboard() {
1240
1731
 
1241
1732
  **Key benefit**: The parent component doesn't re-render when atoms change - only the `rx` components do.
1242
1733
 
1734
+ #### Inline Loading and Error Handling
1735
+
1736
+ `rx()` supports optional `loading` and `error` handlers for inline async state handling without Suspense/ErrorBoundary:
1737
+
1738
+ ```tsx
1739
+ function Dashboard() {
1740
+ return (
1741
+ <div>
1742
+ {/* With loading handler - no Suspense needed */}
1743
+ {rx(userData$, {
1744
+ loading: () => <Spinner />,
1745
+ })}
1746
+
1747
+ {/* With error handler - no ErrorBoundary needed */}
1748
+ {rx(userData$, {
1749
+ error: ({ error }) => <Alert>{String(error)}</Alert>,
1750
+ })}
1751
+
1752
+ {/* Both handlers - fully self-contained */}
1753
+ {rx(userData$, {
1754
+ loading: () => <UserSkeleton />,
1755
+ error: ({ error }) => <UserError error={error} />,
1756
+ })}
1757
+
1758
+ {/* With selector and options */}
1759
+ {rx(({ read }) => read(posts$).slice(0, 5), {
1760
+ loading: () => <PostsSkeleton count={5} />,
1761
+ error: ({ error }) => <PostsError onRetry={() => posts$.refresh()} />,
1762
+ equals: "shallow",
1763
+ })}
1764
+ </div>
1765
+ );
1766
+ }
1767
+ ```
1768
+
1769
+ **When to use each approach:**
1770
+
1771
+ | Approach | Use When |
1772
+ | ----------------------------- | ------------------------------------------------------ |
1773
+ | `rx()` with Suspense | Shared loading UI across multiple components |
1774
+ | `rx()` with `loading`/`error` | Self-contained component with custom inline UI |
1775
+ | `rx()` with `state()` | Complex conditional rendering based on multiple states |
1776
+
1777
+ #### Selector Memoization with `deps`
1778
+
1779
+ By default, function selectors are recreated on every render. Use `deps` to control memoization:
1780
+
1781
+ ```tsx
1782
+ function Component({ multiplier }: { multiplier: number }) {
1783
+ return (
1784
+ <div>
1785
+ {/* No memoization (default) - selector recreated every render */}
1786
+ {rx(({ read }) => read(count$) * 2)}
1787
+ {/* Stable forever - selector never recreated */}
1788
+ {rx(({ read }) => read(count$) * 2, { deps: [] })}
1789
+ {/* Recreate when multiplier changes */}
1790
+ {rx(({ read }) => read(count$) * multiplier, { deps: [multiplier] })}
1791
+ {/* Atom shorthand is always stable by reference */}
1792
+ {rx(count$)} {/* No deps needed */}
1793
+ </div>
1794
+ );
1795
+ }
1796
+ ```
1797
+
1798
+ **Memoization behavior:**
1799
+
1800
+ | Input | `deps` | Behavior |
1801
+ | ------------------------------- | ----------- | --------------------------------- |
1802
+ | Atom (`rx(atom$)`) | (ignored) | Always memoized by atom reference |
1803
+ | Function (`rx(({ read }) => …`) | `undefined` | No memoization (recreated always) |
1804
+ | Function | `[]` | Stable forever (never recreated) |
1805
+ | Function | `[a, b]` | Recreated when deps change |
1806
+
1243
1807
  ### Async Actions with useAction
1244
1808
 
1245
1809
  Handle async operations with built-in loading/error states:
@@ -1285,7 +1849,7 @@ const fetchUser = useAction(
1285
1849
 
1286
1850
  // Re-execute when atom changes
1287
1851
  const fetchPosts = useAction(
1288
- async ({ signal }) => fetchPostsApi(filter$.value, { signal }),
1852
+ async ({ signal }) => fetchPostsApi(filter$.get(), { signal }),
1289
1853
  { lazy: false, deps: [filter$] }
1290
1854
  );
1291
1855
  ```
@@ -1340,13 +1904,13 @@ atomirx is designed to work seamlessly with React Suspense:
1340
1904
  import { Suspense } from "react";
1341
1905
  import { ErrorBoundary } from "react-error-boundary";
1342
1906
  import { atom } from "atomirx";
1343
- import { useValue } from "atomirx/react";
1907
+ import { useSelector } from "atomirx/react";
1344
1908
 
1345
1909
  const user$ = atom(fetchUser());
1346
1910
 
1347
1911
  function UserProfile() {
1348
1912
  // Suspends until user$ resolves
1349
- const user = useValue(user$);
1913
+ const user = useSelector(user$);
1350
1914
  return <div>{user.name}</div>;
1351
1915
  }
1352
1916
 
@@ -1475,34 +2039,104 @@ function isAtom<T>(value: unknown): value is Atom<T>;
1475
2039
 
1476
2040
  ### SelectContext API
1477
2041
 
1478
- Available in `derived()`, `effect()`, `useValue()`, and `rx()`:
2042
+ Available in `derived()`, `effect()`, `useSelector()`, and `rx()`:
1479
2043
 
1480
- | Method | Signature | Description |
1481
- | --------- | ---------------------------------- | -------------------------------------------- |
1482
- | `get` | `<T>(atom: Atom<T>) => Awaited<T>` | Read atom value with dependency tracking |
1483
- | `all` | `(...atoms) => [values...]` | Wait for all atoms (Promise.all semantics) |
1484
- | `any` | `(...atoms) => value` | First ready value (Promise.any semantics) |
1485
- | `race` | `(...atoms) => value` | First settled value (Promise.race semantics) |
1486
- | `settled` | `(...atoms) => SettledResult[]` | All results (Promise.allSettled semantics) |
2044
+ | Method | Signature | Description |
2045
+ | --------- | ----------------------------------------- | ---------------------------------------------------- |
2046
+ | `read` | `<T>(atom: Atom<T>) => Awaited<T>` | Read atom value with dependency tracking |
2047
+ | `ready` | `<T>(atom: Atom<T>) => NonNullable<T>` | Wait for non-null value (only in derived/effect) |
2048
+ | `all` | `(atoms[]) => values[]` | Wait for all atoms (Promise.all semantics) |
2049
+ | `any` | `({ key: atom }) => { key, value }` | First ready value (Promise.any semantics) |
2050
+ | `race` | `({ key: atom }) => { key, value }` | First settled value (Promise.race semantics) |
2051
+ | `settled` | `(atoms[]) => SettledResult[]` | All results (Promise.allSettled semantics) |
2052
+ | `state` | `(atom \| selector) => SelectStateResult` | Get async state without throwing (equality-friendly) |
2053
+ | `safe` | `(fn) => [error, result]` | Catch errors, preserve Suspense |
1487
2054
 
1488
2055
  **Behavior:**
1489
2056
 
1490
- - `get()`: Returns value if ready, throws error if error, throws Promise if loading
2057
+ - `read()`: Returns value if ready, throws error if error, throws Promise if loading
2058
+ - `ready()`: Returns non-null value, suspends if null/undefined (only in derived/effect)
1491
2059
  - `all()`: Suspends until all atoms are ready, throws on first error
1492
2060
  - `any()`: Returns first ready value, throws AggregateError if all error
1493
2061
  - `race()`: Returns first settled (ready or error)
1494
2062
  - `settled()`: Returns `{ status: "ready", value }` or `{ status: "error", error }` for each atom
2063
+ - `state()`: Returns `AtomState<T>` without throwing - useful for inline state handling
2064
+ - `safe()`: Catches errors but re-throws Promises to preserve Suspense
2065
+
2066
+ #### `state()` - Get Async State Without Throwing
2067
+
2068
+ The `state()` method returns an `AtomState<T>` object instead of throwing. This is useful when you want to handle loading/error states inline without Suspense.
2069
+
2070
+ **Available at every level** - derived, rx, useSelector, effect:
2071
+
2072
+ ```typescript
2073
+ // 1. In derived() - Build dashboard with partial loading
2074
+ const dashboard$ = derived(({ state }) => {
2075
+ const userState = state(user$);
2076
+ const postsState = state(posts$);
2077
+
2078
+ return {
2079
+ user: userState.status === 'ready' ? userState.value : null,
2080
+ posts: postsState.status === 'ready' ? postsState.value : [],
2081
+ isLoading: userState.status === 'loading' || postsState.status === 'loading',
2082
+ };
2083
+ });
2084
+
2085
+ // 2. In rx() - Inline loading/error UI
2086
+ {rx(({ state }) => {
2087
+ const dataState = state(data$);
2088
+
2089
+ if (dataState.status === 'loading') return <Spinner />;
2090
+ if (dataState.status === 'error') return <Error error={dataState.error} />;
2091
+ return <Content data={dataState.value} />;
2092
+ })}
2093
+
2094
+ // 3. In useSelector() - Get state object in component
2095
+ function Component() {
2096
+ const dataState = useSelector(({ state }) => state(asyncAtom$));
2097
+
2098
+ if (dataState.status === 'loading') return <Spinner />;
2099
+ // ...
2100
+ }
2101
+
2102
+ // 4. In effect() - React to state changes
2103
+ effect(({ state }) => {
2104
+ const connState = state(connection$);
2105
+
2106
+ if (connState.status === 'ready') {
2107
+ console.log('Connected:', connState.value);
2108
+ }
2109
+ });
2110
+
2111
+ // 5. With combined operations
2112
+ const allData$ = derived(({ state, all }) => {
2113
+ const result = state(() => all([a$, b$, c$]));
2114
+
2115
+ if (result.status === 'loading') return { loading: true };
2116
+ if (result.status === 'error') return { error: result.error };
2117
+ return { data: result.value };
2118
+ });
2119
+ ```
2120
+
2121
+ **`state()` vs `read()`:**
2122
+
2123
+ | Method | On Loading | On Error | On Ready |
2124
+ | --------- | ------------------------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------ |
2125
+ | `read()` | Throws Promise (Suspense) | Throws Error | Returns value |
2126
+ | `state()` | Returns `{ status: "loading", value: undefined, error: undefined }` | Returns `{ status: "error", value: undefined, error }` | Returns `{ status: "ready", value, error: undefined }` |
2127
+
2128
+ **Note:** `state()` returns `SelectStateResult<T>` with all properties always present (`status`, `value`, `error`). This enables easy destructuring and equality comparisons (no promise reference issues).
1495
2129
 
1496
2130
  ### React API
1497
2131
 
1498
- #### `useValue`
2132
+ #### `useSelector`
1499
2133
 
1500
2134
  ```typescript
1501
2135
  // Shorthand
1502
- function useValue<T>(atom: Atom<T>): T;
2136
+ function useSelector<T>(atom: Atom<T>): T;
1503
2137
 
1504
2138
  // Context selector
1505
- function useValue<T>(
2139
+ function useSelector<T>(
1506
2140
  selector: (context: SelectContext) => T,
1507
2141
  equals?: (prev: T, next: T) => boolean
1508
2142
  ): T;
@@ -1513,14 +2147,87 @@ function useValue<T>(
1513
2147
  ```typescript
1514
2148
  // Shorthand
1515
2149
  function rx<T>(atom: Atom<T>): ReactNode;
2150
+ function rx<T>(atom: Atom<T>, options?: RxOptions<T>): ReactNode;
1516
2151
 
1517
2152
  // Context selector
2153
+ function rx<T>(selector: (context: SelectContext) => T): ReactNode;
1518
2154
  function rx<T>(
1519
2155
  selector: (context: SelectContext) => T,
1520
- equals?: (prev: T, next: T) => boolean
2156
+ options?: Equality<T> | RxOptions<T>
1521
2157
  ): ReactNode;
2158
+
2159
+ interface RxOptions<T> {
2160
+ /** Equality function for value comparison */
2161
+ equals?: Equality<T>;
2162
+ /** Render function for loading state (wraps with Suspense) */
2163
+ loading?: () => ReactNode;
2164
+ /** Render function for error state (wraps with ErrorBoundary) */
2165
+ error?: (props: { error: unknown }) => ReactNode;
2166
+ /** Dependencies for selector memoization */
2167
+ deps?: unknown[];
2168
+ }
2169
+ ```
2170
+
2171
+ **Selector memoization with `deps`:**
2172
+
2173
+ ```tsx
2174
+ // No memoization (default) - selector recreated every render
2175
+ rx(({ read }) => read(count$) * multiplier);
2176
+
2177
+ // Stable forever - never recreated
2178
+ rx(({ read }) => read(count$) * 2, { deps: [] });
2179
+
2180
+ // Recreate when multiplier changes
2181
+ rx(({ read }) => read(count$) * multiplier, { deps: [multiplier] });
2182
+
2183
+ // Atom shorthand is always stable (deps ignored)
2184
+ rx(count$);
2185
+ ```
2186
+
2187
+ **With inline loading/error handlers:**
2188
+
2189
+ ```tsx
2190
+ // Loading handler only
2191
+ {
2192
+ rx(asyncAtom$, {
2193
+ loading: () => <Spinner />,
2194
+ });
2195
+ }
2196
+
2197
+ // Error handler only
2198
+ {
2199
+ rx(asyncAtom$, {
2200
+ error: ({ error }) => <Alert>{String(error)}</Alert>,
2201
+ });
2202
+ }
2203
+
2204
+ // Both handlers
2205
+ {
2206
+ rx(asyncAtom$, {
2207
+ loading: () => <Skeleton />,
2208
+ error: ({ error }) => <ErrorMessage error={error} />,
2209
+ });
2210
+ }
2211
+
2212
+ // With selector and options
2213
+ {
2214
+ rx(({ read }) => read(user$).profile, {
2215
+ loading: () => <ProfileSkeleton />,
2216
+ error: ({ error }) => <ProfileError error={error} />,
2217
+ equals: "shallow",
2218
+ });
2219
+ }
1522
2220
  ```
1523
2221
 
2222
+ **Behavior with options:**
2223
+
2224
+ | Options | Loading State | Error State | Ready State |
2225
+ | -------------------- | ------------------ | ---------------- | ------------ |
2226
+ | No options | Suspense | ErrorBoundary | Render value |
2227
+ | `{ loading }` | Render `loading()` | ErrorBoundary | Render value |
2228
+ | `{ error }` | Suspense | Render `error()` | Render value |
2229
+ | `{ loading, error }` | Render `loading()` | Render `error()` | Render value |
2230
+
1524
2231
  #### `useAction`
1525
2232
 
1526
2233
  ```typescript
@@ -1559,7 +2266,7 @@ import { atom, derived } from "atomirx";
1559
2266
  // Types are automatically inferred
1560
2267
  const count$ = atom(0); // MutableAtom<number>
1561
2268
  const name$ = atom("John"); // MutableAtom<string>
1562
- const doubled$ = derived(({ get }) => get(count$) * 2); // Atom<number>
2269
+ const doubled$ = derived(({ read }) => read(count$) * 2); // Atom<number>
1563
2270
 
1564
2271
  // Explicit typing when needed
1565
2272
  interface User {
@@ -1572,8 +2279,8 @@ const user$ = atom<User | null>(null); // MutableAtom<User | null>
1572
2279
  const userData$ = atom<User>(fetchUser()); // MutableAtom<User>
1573
2280
 
1574
2281
  // Type-safe selectors
1575
- const userName$ = derived(({ get }) => {
1576
- const user = get(user$);
2282
+ const userName$ = derived(({ read }) => {
2283
+ const user = read(user$);
1577
2284
  return user?.name ?? "Anonymous"; // Atom<string>
1578
2285
  });
1579
2286
 
@@ -1638,8 +2345,8 @@ const todoList = createListAtom<Todo>();
1638
2345
 
1639
2346
  ### Community
1640
2347
 
1641
- - [GitHub Issues](https://github.com/atomirx/atomirx/issues) - Bug reports and feature requests
1642
- - [Discussions](https://github.com/atomirx/atomirx/discussions) - Questions and community support
2348
+ - [GitHub Issues](https://github.com/linq2js/atomirx/issues) - Bug reports and feature requests
2349
+ - [Discussions](https://github.com/linq2js/atomirx/discussions) - Questions and community support
1643
2350
 
1644
2351
  ## License
1645
2352