atomirx 0.0.7 → 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 -30
  33. package/dist/react/index.js +206 -791
  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 -39
package/README.md CHANGED
@@ -1,2373 +1,337 @@
1
- # Atomirx
2
-
3
- **Opinionated, Batteries-Included Reactive State Management**
4
-
5
- [![npm version](https://img.shields.io/npm/v/atomirx.svg?style=flat-square)](https://www.npmjs.com/package/atomirx)
6
- [![npm downloads](https://img.shields.io/npm/dm/atomirx.svg?style=flat-square)](https://www.npmjs.com/package/atomirx)
7
- [![Bundle Size](https://img.shields.io/bundlephobia/minzip/atomirx?style=flat-square)](https://bundlephobia.com/package/atomirx)
8
- [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue?style=flat-square)](https://www.typescriptlang.org/)
9
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
10
-
11
- ## Purpose
12
-
13
- The **atomirx** package is intended to be the standard way to write reactive state logic in TypeScript and JavaScript applications. It was originally created to help address three common concerns about state management:
14
-
15
- - "Setting up reactive state is too complicated"
16
- - "I have to add a lot of packages to handle async operations"
17
- - "Fine-grained reactivity requires too much boilerplate"
18
-
19
- We can't solve every use case, but in the spirit of [`create-react-app`](https://github.com/facebook/create-react-app), we can provide an official, opinionated set of tools that handles the most common use cases and reduces the decisions you need to make.
20
-
21
- ## Table of Contents
22
-
23
- - [Atomirx](#atomirx)
24
- - [Purpose](#purpose)
25
- - [Table of Contents](#table-of-contents)
26
- - [Installation](#installation)
27
- - [Why atomirx?](#why-atomirx)
28
- - [The Problem](#the-problem)
29
- - [The Solution](#the-solution)
30
- - [Design Philosophy](#design-philosophy)
31
- - [What's Included](#whats-included)
32
- - [Core](#core)
33
- - [React Bindings (`atomirx/react`)](#react-bindings-atomirxreact)
34
- - [Getting Started](#getting-started)
35
- - [Basic Example: Counter](#basic-example-counter)
36
- - [React Example: Todo App](#react-example-todo-app)
37
- - [Patterns \& Best Practices](#patterns--best-practices)
38
- - [Naming: The `$` Suffix](#naming-the--suffix)
39
- - [When to Use Each Primitive](#when-to-use-each-primitive)
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)
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)
54
- - [Usage Guide](#usage-guide)
55
- - [Atoms: The Foundation](#atoms-the-foundation)
56
- - [Creating Atoms](#creating-atoms)
57
- - [Reading Atom Values](#reading-atom-values)
58
- - [Updating Atoms](#updating-atoms)
59
- - [Subscribing to Changes](#subscribing-to-changes)
60
- - [Complete Atom API](#complete-atom-api)
61
- - [Derived State: Computed Values](#derived-state-computed-values)
62
- - [Basic Derived State](#basic-derived-state)
63
- - [Conditional Dependencies](#conditional-dependencies)
64
- - [Suspense-Style Getters](#suspense-style-getters)
65
- - [Derived from Multiple Async Sources](#derived-from-multiple-async-sources)
66
- - [Effects: Side Effect Management](#effects-side-effect-management)
67
- - [Basic Effects](#basic-effects)
68
- - [Effects with Cleanup](#effects-with-cleanup)
69
- - [Effects with Multiple Dependencies](#effects-with-multiple-dependencies)
70
- - [Async Patterns](#async-patterns)
71
- - [`all()` - Wait for All (like Promise.all)](#all---wait-for-all-like-promiseall)
72
- - [`any()` - First Ready (like Promise.any)](#any---first-ready-like-promiseany)
73
- - [`race()` - First Settled (like Promise.race)](#race---first-settled-like-promiserace)
74
- - [`settled()` - All Results (like Promise.allSettled)](#settled---all-results-like-promiseallsettled)
75
- - [Async Utility Summary](#async-utility-summary)
76
- - [Batching Updates](#batching-updates)
77
- - [Event System](#event-system)
78
- - [Dependency Injection](#dependency-injection)
79
- - [Atom Metadata and Middleware](#atom-metadata-and-middleware)
80
- - [Extending AtomMeta with TypeScript](#extending-atommeta-with-typescript)
81
- - [Using onCreateHook for Middleware](#using-oncreatehook-for-middleware)
82
- - [Creating Persisted Atoms](#creating-persisted-atoms)
83
- - [Multiple Middleware Example](#multiple-middleware-example)
84
- - [Hook Info Types](#hook-info-types)
85
- - [React Integration](#react-integration)
86
- - [useSelector Hook](#useselector-hook)
87
- - [Custom Equality](#custom-equality)
88
- - [Why useSelector is Powerful](#why-useselector-is-powerful)
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)
92
- - [Async Actions with useAction](#async-actions-with-useaction)
93
- - [Eager Execution](#eager-execution)
94
- - [useAction API](#useaction-api)
95
- - [Reference Stability with useStable](#reference-stability-with-usestable)
96
- - [Suspense Integration](#suspense-integration)
97
- - [Nested Suspense Boundaries](#nested-suspense-boundaries)
98
- - [API Reference](#api-reference)
99
- - [Core API](#core-api)
100
- - [`atom<T>(initialValue, options?)`](#atomtinitialvalue-options)
101
- - [`derived<T>(selector)`](#derivedtselector)
102
- - [`effect(fn, options?)`](#effectfn-options)
103
- - [`batch(fn)`](#batchfn)
104
- - [`emitter<T>()`](#emittert)
105
- - [`define<T>(factory, options?)`](#definetfactory-options)
106
- - [`isAtom(value)`](#isatomvalue)
107
- - [SelectContext API](#selectcontext-api)
108
- - [`state()` - Get Async State Without Throwing](#state---get-async-state-without-throwing)
109
- - [React API](#react-api)
110
- - [`useSelector`](#useselector)
111
- - [`rx`](#rx)
112
- - [`useAction`](#useaction)
113
- - [`useStable`](#usestable)
114
- - [TypeScript Integration](#typescript-integration)
115
- - [Comparison with Other Libraries](#comparison-with-other-libraries)
116
- - [When to Use atomirx](#when-to-use-atomirx)
117
- - [Resources \& Learning](#resources--learning)
118
- - [Documentation](#documentation)
119
- - [Examples](#examples)
120
- - [Community](#community)
121
- - [License](#license)
122
-
123
- ## Installation
124
-
125
- atomirx is available as a package on NPM for use with a module bundler or in a Node application:
1
+ <div align="center">
126
2
 
127
- ```bash
128
- # NPM
129
- npm install atomirx
130
-
131
- # Yarn
132
- yarn add atomirx
133
-
134
- # PNPM
135
- pnpm add atomirx
136
- ```
137
-
138
- The package includes precompiled ESM and CommonJS builds, along with TypeScript type definitions.
139
-
140
- ## Why atomirx?
141
-
142
- ### The Problem
143
-
144
- Traditional state management solutions often require:
145
-
146
- - **Excessive boilerplate** for simple state updates
147
- - **Separate packages** for async operations, caching, and derived state
148
- - **Manual subscription management** leading to memory leaks
149
- - **Coarse-grained updates** causing unnecessary re-renders
150
- - **Complex mental models** for understanding data flow
151
-
152
- ### The Solution
153
-
154
- atomirx provides a **unified, minimal API** that handles all common state management patterns out of the box:
155
-
156
- | Challenge | atomirx Solution |
157
- | ----------------- | -------------------------------------------------------------- |
158
- | Mutable state | `atom()` - single source of truth with automatic subscriptions |
159
- | Computed values | `derived()` - automatic dependency tracking and memoization |
160
- | Side effects | `effect()` - declarative effects with cleanup |
161
- | Async operations | Built-in Promise support with loading/error states |
162
- | React integration | Suspense-compatible hooks with fine-grained updates |
163
- | Testing | `define()` - dependency injection with easy mocking |
164
-
165
- ### Design Philosophy
166
-
167
- atomirx is built on these core principles:
168
-
169
- 1. **Minimal API Surface** - Learn three functions (`atom`, `derived`, `effect`) and you're productive
170
- 2. **Async-First** - Promises are first-class citizens, not an afterthought
171
- 3. **Fine-Grained Reactivity** - Only the code that needs to run, runs
172
- 4. **Type Safety** - Full TypeScript inference without manual type annotations
173
- 5. **Framework Agnostic** - Core library has zero dependencies; React bindings are optional
174
- 6. **Suspense-Native** - Designed from the ground up for React Suspense patterns
175
-
176
- ## What's Included
177
-
178
- atomirx includes these APIs:
179
-
180
- ### Core
181
-
182
- - **`atom()`**: Creates mutable reactive state containers with built-in async support
183
- - **`derived()`**: Creates computed values with automatic dependency tracking
184
- - **`effect()`**: Runs side effects that automatically re-execute when dependencies change
185
- - **`batch()`**: Groups multiple updates into a single notification cycle
186
- - **`define()`**: Creates swappable lazy singletons for dependency injection
187
-
188
- ### React Bindings (`atomirx/react`)
189
-
190
- - **`useSelector()`**: Subscribe to atoms with automatic re-rendering (Suspense-based)
191
- - **`rx()`**: Inline reactive components with optional loading/error handlers
192
- - **`useAction()`**: Handle async operations with loading/error states
193
- - **`useStable()`**: Stabilize object/array/callback references
194
-
195
- ## Getting Started
196
-
197
- ### Basic Example: Counter
198
-
199
- ```typescript
200
- import { atom, derived, effect } from "atomirx";
201
-
202
- // Step 1: Create an atom (mutable state)
203
- const count$ = atom(0);
204
-
205
- // Step 2: Create derived state (computed values)
206
- const doubled$ = derived(({ read }) => read(count$) * 2);
207
- const message$ = derived(({ read }) => {
208
- const count = read(count$);
209
- return count === 0 ? "Click to start!" : `Count: ${count}`;
210
- });
211
-
212
- // Step 3: React to changes with effects
213
- effect(({ read }) => {
214
- console.log("Current count:", read(count$));
215
- console.log("Doubled value:", read(doubled$));
216
- });
217
-
218
- // Step 4: Update state
219
- count$.set(5); // Logs: Current count: 5, Doubled value: 10
220
- count$.set((n) => n + 1); // Logs: Current count: 6, Doubled value: 12
221
- ```
222
-
223
- ### React Example: Todo App
224
-
225
- ```tsx
226
- import { atom, derived } from "atomirx";
227
- import { useSelector, rx } from "atomirx/react";
228
-
229
- // Define your state
230
- interface Todo {
231
- id: number;
232
- text: string;
233
- completed: boolean;
234
- }
235
-
236
- const todos$ = atom<Todo[]>([]);
237
- const filter$ = atom<"all" | "active" | "completed">("all");
238
-
239
- // Derive computed state
240
- const filteredTodos$ = derived(({ read }) => {
241
- const todos = read(todos$);
242
- const filter = read(filter$);
243
-
244
- switch (filter) {
245
- case "active":
246
- return todos.filter((t) => !t.completed);
247
- case "completed":
248
- return todos.filter((t) => t.completed);
249
- default:
250
- return todos;
251
- }
252
- });
253
-
254
- const stats$ = derived(({ read }) => {
255
- const todos = read(todos$);
256
- return {
257
- total: todos.length,
258
- completed: todos.filter((t) => t.completed).length,
259
- remaining: todos.filter((t) => !t.completed).length,
260
- };
261
- });
262
-
263
- // Actions
264
- const addTodo = (text: string) => {
265
- todos$.set((todos) => [...todos, { id: Date.now(), text, completed: false }]);
266
- };
267
-
268
- const toggleTodo = (id: number) => {
269
- todos$.set((todos) =>
270
- todos.map((t) => (t.id === id ? { ...t, completed: !t.completed } : t))
271
- );
272
- };
273
-
274
- // Components
275
- function TodoList() {
276
- const todos = useSelector(filteredTodos$);
277
-
278
- return (
279
- <ul>
280
- {todos.map((todo) => (
281
- <li key={todo.id} onClick={() => toggleTodo(todo.id)}>
282
- {todo.completed ? "✓" : "○"} {todo.text}
283
- </li>
284
- ))}
285
- </ul>
286
- );
287
- }
288
-
289
- function Stats() {
290
- // Fine-grained updates: only re-renders when stats change
291
- return (
292
- <footer>
293
- {rx(({ read }) => {
294
- const { total, completed, remaining } = read(stats$);
295
- return (
296
- <span>
297
- {remaining} of {total} remaining
298
- </span>
299
- );
300
- })}
301
- </footer>
302
- );
303
- }
304
- ```
305
-
306
- ## Patterns & Best Practices
307
-
308
- Following consistent patterns and best practices makes atomirx code more readable and maintainable across your team.
309
-
310
- ### Naming: The `$` Suffix
311
-
312
- All atoms (both `atom()` and `derived()`) should use the `$` suffix. This convention:
313
-
314
- - Clearly distinguishes reactive state from regular variables
315
- - Makes it obvious when you're working with atoms vs plain values
316
- - Improves code readability at a glance
317
-
318
- ```typescript
319
- // ✅ Good - clear that these are atoms
320
- const count$ = atom(0);
321
- const user$ = atom<User | null>(null);
322
- const filteredItems$ = derived(({ read }) => /* ... */);
323
-
324
- // ❌ Avoid - unclear what's reactive
325
- const count = atom(0);
326
- const user = atom<User | null>(null);
327
- ```
328
-
329
- ### When to Use Each Primitive
330
-
331
- | Primitive | Purpose | Use When |
332
- | ----------- | ----------------------- | --------------------------------------------------------------------------- |
333
- | `atom()` | Store values | You need mutable state (including Promises) |
334
- | `derived()` | Compute reactive values | You need to transform or combine atom values |
335
- | `effect()` | Trigger side effects | You need to react to atom changes (sync to external systems, logging, etc.) |
336
-
337
- ### Atom Storage: Stable Scopes Only
338
-
339
- **Never store atoms in component/local scope.** Atoms created inside React components (even with `useRef`) can lead to:
340
-
341
- - **Memory leaks** - atoms aren't properly disposed when components unmount
342
- - **Forgotten disposal** - easy to forget cleanup logic
343
- - **Multiple instances** - each component render may create new atoms
344
-
345
- ```typescript
346
- // ❌ BAD - atoms in component scope
347
- function TodoList() {
348
- // These atoms are created per component instance!
349
- const todos$ = useRef(atom(() => fetchTodos())).current;
350
- const filter$ = useRef(atom("all")).current;
351
- // Memory leak: atoms not disposed on unmount
352
- }
353
-
354
- // ✅ GOOD - atoms at module scope
355
- const todos$ = atom(() => fetchTodos());
356
- const filter$ = atom("all");
357
-
358
- function TodoList() {
359
- const todos = useSelector(filteredTodos$);
360
- // ...
361
- }
362
- ```
363
-
364
- **Use `define()` to organize atoms into modules:**
365
-
366
- ```typescript
367
- // ✅ BEST - atoms organized in a module with define()
368
- const TodoModule = define(() => {
369
- // Atoms are created once, lazily
370
- const todos$ = atom(() => fetchTodos(), { meta: { key: "todos" } });
371
- const filter$ = atom<"all" | "active" | "completed">("all");
372
-
373
- const filteredTodos$ = derived(({ read }) => {
374
- const filter = read(filter$);
375
- const todos = read(todos$);
376
- return filter === "all" ? todos : todos.filter(/* ... */);
377
- });
378
-
379
- return {
380
- todos$,
381
- filter$,
382
- filteredTodos$,
383
- setFilter: (f: "all" | "active" | "completed") => filter$.set(f),
384
- refetch: () => todos$.set(fetchTodos()),
385
- reset: () => todos$.reset(),
386
- };
387
- });
388
-
389
- // Usage in React
390
- function TodoList() {
391
- const { filteredTodos$, setFilter } = TodoModule();
392
- const todos = useSelector(filteredTodos$);
393
- // ...
394
- }
395
- ```
396
-
397
- Benefits of `define()`:
398
-
399
- - **Lazy singleton** - module created on first access
400
- - **Testable** - use `.override()` to inject mocks
401
- - **Disposable** - use `.invalidate()` to clean up and recreate
402
- - **Organized** - group related atoms and actions together
403
-
404
- **`atom()`** - Store raw values, including Promises:
405
-
406
- ```typescript
407
- // Synchronous values
408
- const filter$ = atom("all");
409
- const count$ = atom(0);
410
-
411
- // Async values - store the Promise directly
412
- const todoList$ = atom(() => fetchAllTodos()); // Lazy fetch on creation
413
- const userData$ = atom(fetchUser(userId)); // Fetch immediately
414
- ```
415
-
416
- **`derived()`** - Handle reactive/async transformations:
417
-
418
- ```typescript
419
- // derived() automatically unwraps Promises from atoms
420
- const filteredTodoList$ = derived(({ read }) => {
421
- const filter = read(filter$);
422
- const todoList = read(todoList$); // Promise is unwrapped automatically!
423
-
424
- switch (filter) {
425
- case "active":
426
- return todoList.filter((t) => !t.completed);
427
- case "completed":
428
- return todoList.filter((t) => t.completed);
429
- default:
430
- return todoList;
431
- }
432
- });
433
- ```
434
-
435
- **`effect()`** - Coordinate updates across multiple atoms:
436
-
437
- ```typescript
438
- // Sync local state to server when it changes
439
- effect(({ read, onCleanup }) => {
440
- const settings = read(settings$);
441
-
442
- const controller = new AbortController();
443
- saveSettingsToServer(settings, { signal: controller.signal });
444
-
445
- onCleanup(() => controller.abort());
446
- });
447
-
448
- // Update multiple atoms based on another atom's change
449
- effect(({ read }) => {
450
- const user = read(currentUser$);
451
-
452
- if (user) {
453
- // Trigger fetches for user-specific data
454
- userPosts$.set(fetchUserPosts(user.id));
455
- userSettings$.set(fetchUserSettings(user.id));
456
- }
457
- });
458
- ```
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
-
653
- ### Complete Example: Todo App with Async
654
-
655
- ```typescript
656
- import { atom, derived } from "atomirx";
657
- import { useSelector, rx } from "atomirx/react";
658
- import { Suspense } from "react";
659
-
660
- // Atoms store values (including Promises)
661
- const filter$ = atom<"all" | "active" | "completed">("all");
662
- const todoList$ = atom(() => fetchAllTodos()); // Lazy init, re-runs on reset()
663
-
664
- // Derived handles reactive transformations (auto-unwraps Promises)
665
- const filteredTodoList$ = derived(({ read }) => {
666
- const filter = read(filter$);
667
- const todoList = read(todoList$); // This is the resolved value, not a Promise!
668
-
669
- switch (filter) {
670
- case "active": return todoList.filter(t => !t.completed);
671
- case "completed": return todoList.filter(t => t.completed);
672
- default: return todoList;
673
- }
674
- });
675
-
676
- // In UI - useSelector suspends until data is ready
677
- function TodoList() {
678
- const filteredTodoList = useSelector(filteredTodoList$);
679
-
680
- return (
681
- <ul>
682
- {filteredTodoList.map(todo => (
683
- <li key={todo.id}>{todo.text}</li>
684
- ))}
685
- </ul>
686
- );
687
- }
688
-
689
- // Or use rx() for inline reactive rendering
690
- function App() {
691
- return (
692
- <Suspense fallback={<div>Loading todos...</div>}>
693
- {rx(({ read }) =>
694
- read(filteredTodoList$).map(todo => <Todo key={todo.id} todo={todo} />)
695
- )}
696
- </Suspense>
697
- );
698
- }
699
-
700
- // Refetch todos
701
- function RefreshButton() {
702
- return (
703
- <button onClick={() => todoList$.reset()}>
704
- Refresh
705
- </button>
706
- );
707
- }
708
- ```
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
-
933
- ## Usage Guide
934
-
935
- ### Atoms: The Foundation
936
-
937
- Atoms are the building blocks of atomirx. They hold mutable state and automatically notify subscribers when the value changes.
938
-
939
- #### Creating Atoms
940
-
941
- ```typescript
942
- import { atom } from "atomirx";
943
-
944
- // Synchronous atom with initial value
945
- const count$ = atom(0);
946
- const user$ = atom<User | null>(null);
947
- const settings$ = atom({ theme: "dark", language: "en" });
948
-
949
- // Async atom - automatically tracks loading/error states
950
- const userData$ = atom(fetchUser(userId));
951
-
952
- // Lazy initialization - computation deferred until first access
953
- const expensive$ = atom(() => computeExpensiveValue());
954
-
955
- // With fallback value during loading/error
956
- const posts$ = atom(fetchPosts(), { fallback: [] });
957
- ```
958
-
959
- #### Reading Atom Values
960
-
961
- ```typescript
962
- import { getAtomState, isPending } from "atomirx";
963
-
964
- // Direct access (outside reactive context)
965
- console.log(count$.get()); // Current value (T or Promise<T>)
966
-
967
- // Check atom state
968
- const state = getAtomState(userData$);
969
- if (state.status === "loading") {
970
- console.log("Loading...");
971
- } else if (state.status === "error") {
972
- console.log("Error:", state.error);
973
- } else {
974
- console.log("User:", state.value);
975
- }
976
-
977
- // Quick loading check
978
- console.log(isPending(userData$.get())); // true while Promise is pending
979
- ```
980
-
981
- #### Updating Atoms
982
-
983
- ```typescript
984
- // Direct value
985
- count$.set(10);
986
-
987
- // Functional update (receives current value)
988
- count$.set((current) => current + 1);
989
-
990
- // Async update
991
- userData$.set(fetchUser(newUserId));
992
-
993
- // Reset to initial state
994
- count$.reset();
995
- ```
996
-
997
- #### Subscribing to Changes
998
-
999
- ```typescript
1000
- // Subscribe to changes
1001
- const unsubscribe = count$.on((newValue) => {
1002
- console.log("Count changed to:", newValue);
1003
- });
1004
-
1005
- // Await async atoms
1006
- await userData$;
1007
- console.log("User loaded:", userData$.get());
1008
-
1009
- // Unsubscribe when done
1010
- unsubscribe();
1011
- ```
1012
-
1013
- #### Complete Atom API
1014
-
1015
- **MutableAtom** (created by `atom()`):
1016
-
1017
- | Property/Method | Type | Description |
1018
- | --------------- | ------------ | -------------------------------------------------- |
1019
- | `get()` | `T` | Current value (may be a Promise for async atoms) |
1020
- | `set(value)` | `void` | Update with value, Promise, or updater function |
1021
- | `reset()` | `void` | Reset to initial value |
1022
- | `on(listener)` | `() => void` | Subscribe to changes, returns unsubscribe function |
1023
-
1024
- **DerivedAtom** (created by `derived()`):
1025
-
1026
- | Property/Method | Type | Description |
1027
- | --------------- | ---------------- | ---------------------------------------------- |
1028
- | `get()` | `Promise<T>` | Always returns a Promise |
1029
- | `staleValue` | `T \| undefined` | Fallback or last resolved value during loading |
1030
- | `state()` | `AtomState<T>` | Current state (ready/error/loading) |
1031
- | `refresh()` | `void` | Re-run the computation |
1032
- | `on(listener)` | `() => void` | Subscribe to changes, returns unsubscribe |
1033
-
1034
- **AtomState** (returned by `state()` or `getAtomState()`):
1035
-
1036
- ```typescript
1037
- type AtomState<T> =
1038
- | { status: "ready"; value: T }
1039
- | { status: "error"; error: unknown }
1040
- | { status: "loading"; promise: Promise<T> };
1041
- ```
1042
-
1043
- ### Derived State: Computed Values
1044
-
1045
- Derived atoms automatically compute values based on other atoms. They track dependencies at runtime and only recompute when those specific dependencies change.
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
-
1049
- #### Basic Derived State
1050
-
1051
- ```typescript
1052
- import { atom, derived } from "atomirx";
1053
-
1054
- const firstName$ = atom("John");
1055
- const lastName$ = atom("Doe");
1056
-
1057
- // Derived state with automatic dependency tracking
1058
- const fullName$ = derived(({ read }) => {
1059
- return `${read(firstName$)} ${read(lastName$)}`;
1060
- });
1061
-
1062
- // Derived atoms always return Promise<T> for .get()
1063
- await fullName$.get(); // "John Doe"
1064
-
1065
- // Or use staleValue for synchronous access (after first resolution)
1066
- fullName$.staleValue; // "John Doe" (or undefined before first resolution)
1067
-
1068
- // Check state
1069
- fullName$.state(); // { status: "ready", value: "John Doe" }
1070
-
1071
- firstName$.set("Jane");
1072
- await fullName$.get(); // "Jane Doe"
1073
- ```
1074
-
1075
- #### Conditional Dependencies
1076
-
1077
- One of atomirx's most powerful features is **conditional dependency tracking**. Dependencies are tracked based on actual runtime access, not static analysis:
1078
-
1079
- ```typescript
1080
- const showDetails$ = atom(false);
1081
- const summary$ = atom("Brief overview");
1082
- const details$ = atom("Detailed information...");
1083
-
1084
- const content$ = derived(({ read }) => {
1085
- // Only tracks showDetails$ initially
1086
- if (read(showDetails$)) {
1087
- // details$ becomes a dependency only when showDetails$ is true
1088
- return read(details$);
1089
- }
1090
- return read(summary$);
1091
- });
1092
-
1093
- // When showDetails$ is false:
1094
- // - Changes to details$ do NOT trigger recomputation
1095
- // - Only changes to showDetails$ or summary$ trigger recomputation
1096
- ```
1097
-
1098
- #### Suspense-Style Getters
1099
-
1100
- The `read()` function follows React Suspense semantics for async atoms:
1101
-
1102
- | Atom State | `read()` Behavior |
1103
- | ---------- | ----------------------------------------------- |
1104
- | Loading | Throws the Promise (caught by derived/Suspense) |
1105
- | Error | Throws the error |
1106
- | Ready | Returns the value |
1107
-
1108
- ```typescript
1109
- const user$ = atom(fetchUser());
1110
-
1111
- const userName$ = derived(({ read }) => {
1112
- // Automatically handles loading/error states
1113
- const user = read(user$);
1114
- return user.name;
1115
- });
1116
-
1117
- // Check state
1118
- const state = userName$.state();
1119
- if (state.status === "loading") {
1120
- console.log("Loading...");
1121
- } else if (state.status === "error") {
1122
- console.log("Error:", state.error);
1123
- } else {
1124
- console.log("User name:", state.value);
1125
- }
1126
-
1127
- // Or use staleValue with a fallback
1128
- const userName = derived(({ read }) => read(user$).name, { fallback: "Guest" });
1129
- userName.staleValue; // "Guest" during loading, then actual name
1130
- ```
1131
-
1132
- #### Derived from Multiple Async Sources
1133
-
1134
- ```typescript
1135
- const user$ = atom(fetchUser());
1136
- const posts$ = atom(fetchPosts());
1137
-
1138
- const dashboard$ = derived(({ read }) => {
1139
- const user = read(user$); // Suspends if loading
1140
- const posts = read(posts$); // Suspends if loading
1141
-
1142
- return {
1143
- userName: user.name,
1144
- postCount: posts.length,
1145
- };
1146
- });
1147
-
1148
- // Check state
1149
- const state = dashboard$.state();
1150
- // state.status is "loading" until BOTH user$ and posts$ resolve
1151
-
1152
- // Or use all() for explicit parallel loading
1153
- const dashboard2$ = derived(({ all }) => {
1154
- const [user, posts] = all([user$, posts$]);
1155
- return { userName: user.name, postCount: posts.length };
1156
- });
1157
- ```
1158
-
1159
- ### Effects: Side Effect Management
1160
-
1161
- Effects run side effects whenever their dependencies change. They use the same reactive context as `derived()`.
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
-
1165
- #### Basic Effects
1166
-
1167
- ```typescript
1168
- import { atom, effect } from "atomirx";
1169
-
1170
- const count$ = atom(0);
1171
-
1172
- // Effect runs immediately and on every change
1173
- const dispose = effect(({ read }) => {
1174
- console.log("Count is now:", read(count$));
1175
- });
1176
-
1177
- count$.set(5); // Logs: "Count is now: 5"
1178
-
1179
- // Clean up when done
1180
- dispose();
1181
- ```
1182
-
1183
- #### Effects with Cleanup
1184
-
1185
- Use `onCleanup()` to register cleanup functions that run before the next execution or on dispose:
1186
-
1187
- ```typescript
1188
- const interval$ = atom(1000);
1189
-
1190
- const dispose = effect(({ read, onCleanup }) => {
1191
- const ms = read(interval$);
1192
- const id = setInterval(() => console.log("tick"), ms);
1193
-
1194
- // Cleanup runs before next execution or on dispose
1195
- onCleanup(() => clearInterval(id));
1196
- });
1197
-
1198
- interval$.set(500); // Clears old interval, starts new one
1199
- dispose(); // Clears interval completely
1200
- ```
1201
-
1202
- #### Effects with Multiple Dependencies
1203
-
1204
- ```typescript
1205
- const user$ = atom<User | null>(null);
1206
- const settings$ = atom({ notifications: true });
1207
-
1208
- effect(({ read }) => {
1209
- const user = read(user$);
1210
- const settings = read(settings$);
1211
-
1212
- if (user && settings.notifications) {
1213
- analytics.identify(user.id);
1214
- notifications.subscribe(user.id);
1215
- }
1216
- });
1217
- ```
1218
-
1219
- ### Async Patterns
1220
-
1221
- atomirx provides powerful utilities for working with multiple async atoms through the `SelectContext`.
1222
-
1223
- #### `all()` - Wait for All (like Promise.all)
1224
-
1225
- ```typescript
1226
- const user$ = atom(fetchUser());
1227
- const posts$ = atom(fetchPosts());
1228
- const comments$ = atom(fetchComments());
3
+ # atomirx
1229
4
 
1230
- const dashboard$ = derived(({ all }) => {
1231
- // Suspends until ALL atoms resolve (array-based)
1232
- const [user, posts, comments] = all([user$, posts$, comments$]);
5
+ ### Reactive State That Just Works
1233
6
 
1234
- return { user, posts, comments };
1235
- });
1236
- ```
1237
-
1238
- #### `any()` - First Ready (like Promise.any)
1239
-
1240
- ```typescript
1241
- const primaryApi$ = atom(fetchFromPrimary());
1242
- const fallbackApi$ = atom(fetchFromFallback());
1243
-
1244
- const data$ = derived(({ any }) => {
1245
- // Returns first successfully resolved value (object-based, returns { key, value })
1246
- const result = any({ primary: primaryApi$, fallback: fallbackApi$ });
1247
- return result.value;
1248
- });
1249
- ```
1250
-
1251
- #### `race()` - First Settled (like Promise.race)
1252
-
1253
- ```typescript
1254
- const cache$ = atom(checkCache());
1255
- const api$ = atom(fetchFromApi());
1256
-
1257
- const data$ = derived(({ race }) => {
1258
- // Returns first settled (ready OR error) (object-based, returns { key, value })
1259
- const result = race({ cache: cache$, api: api$ });
1260
- return result.value;
1261
- });
1262
- ```
1263
-
1264
- #### `settled()` - All Results (like Promise.allSettled)
1265
-
1266
- ```typescript
1267
- const user$ = atom(fetchUser());
1268
- const posts$ = atom(fetchPosts());
1269
-
1270
- const results$ = derived(({ settled }) => {
1271
- // Returns status for each atom (array-based)
1272
- const [userResult, postsResult] = settled([user$, posts$]);
7
+ [![npm](https://img.shields.io/npm/v/atomirx.svg?style=flat-square&color=blue)](https://www.npmjs.com/package/atomirx)
8
+ [![bundle](https://img.shields.io/bundlephobia/minzip/atomirx?style=flat-square&color=green)](https://bundlephobia.com/package/atomirx)
9
+ [![typescript](https://img.shields.io/badge/TypeScript-Ready-blue?style=flat-square)](https://www.typescriptlang.org/)
1273
10
 
1274
- return {
1275
- user: userResult.status === "ready" ? userResult.value : null,
1276
- posts: postsResult.status === "ready" ? postsResult.value : [],
1277
- hasErrors: userResult.status === "error" || postsResult.status === "error",
1278
- };
1279
- });
1280
- ```
1281
-
1282
- #### Async Utility Summary
1283
-
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 |
11
+ **Atoms** · **Derived** · **Effects** · **Suspense** · **DevTools**
1290
12
 
1291
- **SettledResult type:**
13
+ </div>
1292
14
 
1293
- ```typescript
1294
- type SettledResult<T> =
1295
- | { status: "ready"; value: T }
1296
- | { status: "error"; error: unknown };
1297
- ```
1298
-
1299
- ### Batching Updates
15
+ ---
1300
16
 
1301
- When updating multiple atoms, use `batch()` to combine notifications:
1302
-
1303
- ```typescript
1304
- import { atom, batch } from "atomirx";
17
+ ```tsx
18
+ import { atom, derived } from "atomirx";
19
+ import { useSelector } from "atomirx/react";
1305
20
 
1306
- const firstName$ = atom("John");
1307
- const lastName$ = atom("Doe");
1308
- const age$ = atom(30);
21
+ const count$ = atom(0);
22
+ const doubled$ = derived(({ read }) => read(count$) * 2);
1309
23
 
1310
- // Without batch: 3 separate notifications
1311
- firstName$.set("Jane");
1312
- lastName$.set("Smith");
1313
- age$.set(25);
24
+ function App() {
25
+ // Read multiple atoms in one selector = one subscription, one re-render
26
+ const { count, doubled } = useSelector(({ read }) => ({
27
+ count: read(count$),
28
+ doubled: read(doubled$),
29
+ }));
1314
30
 
1315
- // With batch: 1 notification with final state
1316
- batch(() => {
1317
- firstName$.set("Jane");
1318
- lastName$.set("Smith");
1319
- age$.set(25);
1320
- });
31
+ return (
32
+ <button onClick={() => count$.set((c) => c + 1)}>
33
+ {count} × 2 = {doubled}
34
+ </button>
35
+ );
36
+ }
1321
37
  ```
1322
38
 
1323
- ### Event System
39
+ ---
1324
40
 
1325
- The `emitter()` function provides a lightweight pub/sub system:
1326
-
1327
- ```typescript
1328
- import { emitter } from "atomirx";
41
+ ## Why atomirx?
1329
42
 
1330
- // Create typed emitter
1331
- const userEvents = emitter<{ type: "login" | "logout"; userId: string }>();
43
+ | Feature | atomirx | Others (Recoil/Jotai/etc) |
44
+ | --------------------- | ---------------------------- | ------------------------------- |
45
+ | **Philosophy** | **Mutable & Synchronous** | often Immutable or Async-forced |
46
+ | **Simple atoms** | `atom(0)` | `atom(0)` |
47
+ | **Computed values** | `derived(({ read }) => ...)` | `atom((get) => ...)` |
48
+ | **Async first-class** | **Built-in Suspense** | Plugin / Add-on |
49
+ | **Side effects** | `effect(({ read }) => ...)` | often `useEffect` |
50
+ | **Leak-free Pools** | **Yes (Auto GC)** | Manual management |
51
+ | **DevTools** | **Zero-config** | Additional setup |
52
+ | **Bundle size** | **Tiny** | Varies |
1332
53
 
1333
- // Subscribe
1334
- const unsubscribe = userEvents.on((event) => {
1335
- console.log(`User ${event.userId} ${event.type}`);
1336
- });
54
+ ---
1337
55
 
1338
- // Emit events
1339
- userEvents.emit({ type: "login", userId: "123" });
56
+ ## Install
1340
57
 
1341
- // Settle pattern - late subscribers receive the settled value
1342
- const appReady = emitter<Config>();
1343
- appReady.settle(config); // All current AND future subscribers receive config
1344
- ```
58
+ <div align="center">
1345
59
 
1346
- ### Dependency Injection
60
+ | Package Manager | Command |
61
+ | :-------------- | :-------------------- |
62
+ | **npm** | `npm install atomirx` |
63
+ | **pnpm** | `pnpm add atomirx` |
64
+ | **yarn** | `yarn add atomirx` |
65
+ | **bun** | `bun add atomirx` |
1347
66
 
1348
- The `define()` function creates swappable lazy singletons, perfect for testing:
67
+ </div>
1349
68
 
1350
- ```typescript
1351
- import { define, atom } from "atomirx";
69
+ ---
1352
70
 
1353
- // Define a store factory
1354
- const counterStore = define(() => {
1355
- const count$ = atom(0);
71
+ ## The Basics
1356
72
 
1357
- return {
1358
- count$,
1359
- increment: () => count$.set((c) => c + 1),
1360
- decrement: () => count$.set((c) => c - 1),
1361
- reset: () => count$.reset(),
1362
- };
1363
- });
73
+ ### Atoms
1364
74
 
1365
- // Usage - lazy singleton (same instance everywhere)
1366
- const store = counterStore();
1367
- store.increment();
75
+ Reactive state containers - the foundation of everything.
1368
76
 
1369
- // Testing - override the factory
1370
- counterStore.override(() => ({
1371
- count$: atom(999),
1372
- increment: vi.fn(),
1373
- decrement: vi.fn(),
1374
- reset: vi.fn(),
1375
- }));
77
+ ```ts
78
+ import { atom } from "atomirx";
1376
79
 
1377
- // Reset to original implementation
1378
- counterStore.reset();
1379
- ```
80
+ const count$ = atom(0);
1380
81
 
1381
- ### Atom Metadata and Middleware
1382
-
1383
- atomirx supports custom metadata on atoms via the `meta` option. Combined with `onCreateHook`, you can implement cross-cutting concerns like persistence, logging, or validation.
1384
-
1385
- #### Extending AtomMeta with TypeScript
1386
-
1387
- Use TypeScript's module augmentation to add custom properties to `AtomMeta`:
1388
-
1389
- ```typescript
1390
- // Extend the meta interfaces with your custom properties
1391
- declare module "atomirx" {
1392
- // MutableAtomMeta - for atom() specific options
1393
- interface MutableAtomMeta {
1394
- /** Whether the atom should be persisted to localStorage */
1395
- persisted?: boolean;
1396
- /**
1397
- * Custom validation function.
1398
- * Return true to allow the update, false to reject it.
1399
- */
1400
- validate?: (value: unknown) => boolean;
1401
- /** Optional error handler for validation failures */
1402
- onValidationError?: (value: unknown, key?: string) => void;
1403
- }
1404
-
1405
- // DerivedAtomMeta - for derived() specific options
1406
- interface DerivedAtomMeta {
1407
- /** Custom cache key for memoization */
1408
- cacheKey?: string;
1409
- }
1410
-
1411
- // AtomMeta - base type, shared by both (key, etc.)
1412
- // interface AtomMeta { ... }
1413
- }
82
+ count$.get(); // Read current value
83
+ count$.set(5); // Set new value
84
+ count$.set(n => n + 1); // Update with reducer
85
+ count$.on(() => { ... }); // Subscribe to changes
1414
86
  ```
1415
87
 
1416
- #### Using onCreateHook for Middleware
1417
-
1418
- The `onCreateHook` fires whenever an atom or module is created. Use the reducer pattern to compose multiple middlewares:
1419
-
1420
- ```typescript
1421
- import { onCreateHook } from "atomirx";
1422
-
1423
- // Persistence middleware
1424
- onCreateHook.override((prev) => (info) => {
1425
- // Call previous middleware first (composition)
1426
- prev?.(info);
1427
-
1428
- // Only handle mutable atoms with persisted flag
1429
- if (info.type === "mutable" && info.meta?.persisted && info.meta?.key) {
1430
- const storageKey = `my-app-${info.meta.key}`;
1431
-
1432
- // Restore from localStorage on creation (if not dirty)
1433
- if (!info.atom.dirty()) {
1434
- const stored = localStorage.getItem(storageKey);
1435
- if (stored) {
1436
- try {
1437
- info.atom.set(JSON.parse(stored));
1438
- } catch {
1439
- // Invalid JSON, ignore
1440
- }
1441
- }
1442
- }
1443
-
1444
- // Save to localStorage on every change
1445
- info.atom.on(() => {
1446
- localStorage.setItem(storageKey, JSON.stringify(info.atom.get()));
1447
- });
1448
- }
1449
- });
1450
- ```
88
+ ### Derived
1451
89
 
1452
- #### Creating Persisted Atoms
90
+ Computed values that auto-update when dependencies change.
1453
91
 
1454
- Now atoms with `persisted: true` automatically sync with localStorage:
92
+ ```ts
93
+ import { atom, derived } from "atomirx";
1455
94
 
1456
- ```typescript
1457
- // This atom will persist across page reloads
1458
- const settings$ = atom(
1459
- { theme: "dark", language: "en" },
1460
- { meta: { key: "settings", persisted: true } }
95
+ // Automatically recomputes when firstName$ or lastName$ changes
96
+ const fullName$ = derived(
97
+ ({ read }) => `${read(firstName$)} ${read(lastName$)}`,
1461
98
  );
1462
99
 
1463
- // Changes are automatically saved
1464
- settings$.set({ theme: "light", language: "en" });
1465
-
1466
- // On next page load, value is restored from localStorage
100
+ await fullName$.get(); // "John Doe"
1467
101
  ```
1468
102
 
1469
- #### Multiple Middleware Example
103
+ ### Effects
1470
104
 
1471
- Compose multiple middlewares using the reducer pattern:
105
+ Side effects that run when dependencies change.
1472
106
 
1473
- ```typescript
1474
- // Logging middleware
1475
- onCreateHook.override((prev) => (info) => {
1476
- prev?.(info);
1477
- console.log(
1478
- `[atomirx] Created ${info.type}: ${info.meta?.key ?? "anonymous"}`
1479
- );
1480
- });
1481
-
1482
- // Validation middleware - wraps set() to validate before applying
1483
- onCreateHook.override((prev) => (info) => {
1484
- prev?.(info);
1485
-
1486
- if (info.type === "mutable" && info.meta?.validate) {
1487
- const validate = info.meta.validate;
1488
- const originalSet = info.atom.set.bind(info.atom);
1489
-
1490
- // Wrap set() with validation
1491
- info.atom.set = (valueOrReducer) => {
1492
- // Resolve the next value (handle both direct value and reducer)
1493
- const nextValue =
1494
- typeof valueOrReducer === "function"
1495
- ? (valueOrReducer as (prev: unknown) => unknown)(info.atom.get())
1496
- : valueOrReducer;
1497
-
1498
- // Validate before applying
1499
- if (!validate(nextValue)) {
1500
- console.warn(
1501
- `[atomirx] Validation failed for ${info.meta?.key}`,
1502
- nextValue
1503
- );
1504
- return; // Reject the update
1505
- }
1506
-
1507
- originalSet(valueOrReducer);
1508
- };
1509
- }
1510
- });
1511
-
1512
- // Usage: atom with validation
1513
- const age$ = atom(25, {
1514
- meta: {
1515
- key: "age",
1516
- validate: (value) =>
1517
- typeof value === "number" && value >= 0 && value <= 150,
1518
- },
1519
- });
1520
-
1521
- age$.set(30); // OK
1522
- age$.set(-5); // Rejected: "Validation failed for age"
1523
- age$.set(200); // Rejected: "Validation failed for age"
1524
- ```
107
+ ```ts
108
+ import { effect } from "atomirx";
1525
109
 
1526
- > **Note:** This validation approach intercepts `set()` at runtime. For compile-time type safety, use TypeScript's type system. This pattern is useful for runtime constraints like ranges, formats, or business rules that can't be expressed in types.
110
+ effect(({ read, onCleanup }) => {
111
+ const config = read(config$); // Subscribe to config changes
1527
112
 
1528
- ```typescript
1529
- // DevTools middleware
1530
- onCreateHook.override((prev) => (info) => {
1531
- prev?.(info);
113
+ // Set up connection based on reactive config
114
+ const socket = connectToSocket(config.url);
115
+ console.log("Connected to", config.url);
1532
116
 
1533
- if (info.type === "mutable" || info.type === "derived") {
1534
- // Register with your devtools
1535
- window.__ATOMIRX_DEVTOOLS__?.register(info);
1536
- }
117
+ // Cleanup runs before next run and on dispose
118
+ onCleanup(() => {
119
+ socket.disconnect();
120
+ console.log("Disconnected");
121
+ });
1537
122
  });
1538
123
  ```
1539
124
 
1540
- #### Hook Info Types
1541
-
1542
- The `onCreateHook` receives different info objects based on what's being created:
1543
-
1544
- ```typescript
1545
- // Mutable atom
1546
- interface MutableCreateInfo {
1547
- type: "mutable";
1548
- key: string | undefined;
1549
- meta: AtomMeta | undefined;
1550
- atom: MutableAtom<unknown>;
1551
- }
1552
-
1553
- // Derived atom
1554
- interface DerivedCreateInfo {
1555
- type: "derived";
1556
- key: string | undefined;
1557
- meta: AtomMeta | undefined;
1558
- atom: DerivedAtom<unknown, boolean>;
1559
- }
1560
-
1561
- // Module (from define())
1562
- interface ModuleCreateInfo {
1563
- type: "module";
1564
- key: string | undefined;
1565
- meta: ModuleMeta | undefined;
1566
- module: unknown;
1567
- }
1568
- ```
1569
-
1570
- ## React Integration
1571
-
1572
- atomirx provides first-class React integration through the `atomirx/react` package.
125
+ ---
1573
126
 
1574
- ### useSelector Hook
127
+ ## React
1575
128
 
1576
- Subscribe to atom values with automatic re-rendering.
129
+ ### useSelector
1577
130
 
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).
131
+ Subscribe to atoms in React components.
1579
132
 
1580
133
  ```tsx
1581
134
  import { useSelector } from "atomirx/react";
1582
- import { atom } from "atomirx";
1583
-
1584
- const count$ = atom(0);
1585
- const user$ = atom<User | null>(null);
1586
135
 
1587
136
  function Counter() {
1588
- // Shorthand: pass atom directly
137
+ // Single atom - component re-renders when count$ changes
1589
138
  const count = useSelector(count$);
1590
139
 
1591
- // Context selector: compute derived value
140
+ // Selector function - derive values inline
1592
141
  const doubled = useSelector(({ read }) => read(count$) * 2);
1593
142
 
1594
- // Multiple atoms
1595
- const display = useSelector(({ read }) => {
1596
- const count = read(count$);
1597
- const user = read(user$);
1598
- return user ? `${user.name}: ${count}` : `Anonymous: ${count}`;
1599
- });
1600
-
1601
- return <div>{display}</div>;
143
+ return (
144
+ <span>
145
+ {count} / {doubled}
146
+ </span>
147
+ );
1602
148
  }
1603
149
  ```
1604
150
 
1605
- #### Custom Equality
1606
-
1607
- ```tsx
1608
- // Only re-render when specific fields change
1609
- const userName = useSelector(
1610
- ({ read }) => read(user$)?.name,
1611
- (prev, next) => prev === next
1612
- );
1613
- ```
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
-
1698
- ### Reactive Components with rx
1699
-
1700
- The `rx()` function creates inline reactive components for fine-grained updates.
151
+ ### rx - Inline Reactive
1701
152
 
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).
153
+ Fine-grained updates without re-rendering the parent component.
1703
154
 
1704
155
  ```tsx
1705
156
  import { rx } from "atomirx/react";
1706
157
 
1707
- function Dashboard() {
158
+ function App() {
1708
159
  return (
1709
160
  <div>
1710
161
  {/* Only this span re-renders when count$ changes */}
1711
- <span>Count: {rx(count$)}</span>
1712
-
1713
- {/* Derived value */}
1714
- <span>Doubled: {rx(({ read }) => read(count$) * 2)}</span>
1715
-
1716
- {/* Complex rendering */}
1717
- {rx(({ read }) => {
1718
- const user = read(user$);
1719
- return user ? <UserCard user={user} /> : <LoginPrompt />;
1720
- })}
1721
-
1722
- {/* Async with utilities */}
1723
- {rx(({ all }) => {
1724
- const [user, posts] = all([user$, posts$]);
1725
- return <Feed user={user} posts={posts} />;
1726
- })}
162
+ Count: {rx(count$)}
163
+ Double: {rx(({ read }) => read(count$) * 2)}
1727
164
  </div>
1728
165
  );
1729
166
  }
1730
167
  ```
1731
168
 
1732
- **Key benefit**: The parent component doesn't re-render when atoms change - only the `rx` components do.
169
+ ---
1733
170
 
1734
- #### Inline Loading and Error Handling
171
+ ## Async Atoms
1735
172
 
1736
- `rx()` supports optional `loading` and `error` handlers for inline async state handling without Suspense/ErrorBoundary:
173
+ First-class async support with React Suspense.
1737
174
 
1738
175
  ```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:
176
+ // Atom holds a Promise - Suspense handles loading state
177
+ const user$ = atom(fetchUser());
1780
178
 
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
- );
179
+ function Profile() {
180
+ const user = useSelector(user$); // Suspends until resolved
181
+ return <h1>{user.name}</h1>;
1795
182
  }
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
-
1807
- ### Async Actions with useAction
1808
-
1809
- Handle async operations with built-in loading/error states:
1810
-
1811
- ```tsx
1812
- import { useAction } from "atomirx/react";
1813
-
1814
- function UserProfile({ userId }: { userId: string }) {
1815
- const saveUser = useAction(async ({ signal }, data: UserData) => {
1816
- const response = await fetch(`/api/users/${userId}`, {
1817
- method: "PUT",
1818
- body: JSON.stringify(data),
1819
- signal, // Automatic abort on unmount or re-execution
1820
- });
1821
- return response.json();
1822
- });
1823
183
 
1824
- return (
1825
- <form
1826
- onSubmit={(e) => {
1827
- e.preventDefault();
1828
- saveUser(formData);
1829
- }}
1830
- >
1831
- {saveUser.status === "loading" && <Spinner />}
1832
- {saveUser.status === "error" && <Error message={saveUser.error} />}
1833
- {saveUser.status === "success" && <Success data={saveUser.result} />}
1834
-
1835
- <button disabled={saveUser.status === "loading"}>Save</button>
1836
- </form>
1837
- );
1838
- }
184
+ // Wrap with Suspense for loading fallback
185
+ <Suspense fallback={<Loading />}>
186
+ <Profile />
187
+ </Suspense>;
1839
188
  ```
1840
189
 
1841
- #### Eager Execution
190
+ ### Multiple Async
1842
191
 
1843
- ```tsx
1844
- // Execute immediately on mount
1845
- const fetchUser = useAction(
1846
- async ({ signal }) => fetchUserApi(userId, { signal }),
1847
- { lazy: false, deps: [userId] }
1848
- );
192
+ Wait for multiple atoms with `all()`.
1849
193
 
1850
- // Re-execute when atom changes
1851
- const fetchPosts = useAction(
1852
- async ({ signal }) => fetchPostsApi(filter$.get(), { signal }),
1853
- { lazy: false, deps: [filter$] }
1854
- );
194
+ ```ts
195
+ derived(({ read, all }) => {
196
+ // Waits for both to resolve (like Promise.all)
197
+ const [user, posts] = all([user$, posts$]);
198
+ return { user, posts };
199
+ });
1855
200
  ```
1856
201
 
1857
- #### useAction API
1858
-
1859
- | Property/Method | Type | Description |
1860
- | --------------- | --------------------------------------------- | ---------------------------- |
1861
- | `status` | `'idle' \| 'loading' \| 'success' \| 'error'` | Current state |
1862
- | `result` | `T \| undefined` | Result value when successful |
1863
- | `error` | `unknown` | Error when failed |
1864
- | `abort()` | `() => void` | Cancel current request |
1865
- | `reset()` | `() => void` | Reset to idle state |
1866
-
1867
- ### Reference Stability with useStable
202
+ ---
1868
203
 
1869
- Prevent unnecessary re-renders by stabilizing references:
204
+ ## Pools (Memory-Safe Families)
1870
205
 
1871
- ```tsx
1872
- import { useStable } from "atomirx/react";
1873
-
1874
- function SearchResults({ query, filters }: Props) {
1875
- const stable = useStable({
1876
- // Object - stable if shallow equal
1877
- searchParams: { query, ...filters },
206
+ Parameterized atoms with automatic garbage collection.
1878
207
 
1879
- // Array - stable if items are reference-equal
1880
- selectedIds: [1, 2, 3],
208
+ **Problem:** Standard "atom families" (atoms created per ID) often leak memory because they leave orphan atoms behind when no longer needed.
1881
209
 
1882
- // Function - reference never changes
1883
- onSelect: (id: number) => {
1884
- console.log("Selected:", id);
1885
- },
1886
- });
210
+ **Solution:** `atomirx` Pools automatically garbage collect entries that haven't been used for a specific time (`gcTime`).
1887
211
 
1888
- // Safe to use in dependency arrays
1889
- useEffect(() => {
1890
- performSearch(stable.searchParams);
1891
- }, [stable.searchParams]);
212
+ ```ts
213
+ import { pool } from "atomirx";
1892
214
 
1893
- return (
1894
- <MemoizedList params={stable.searchParams} onSelect={stable.onSelect} />
1895
- );
1896
- }
215
+ // Create a pool - atoms are created lazily per params
216
+ const userPool = pool(
217
+ (id: string) => fetchUser(id), // Factory function
218
+ { gcTime: 60_000 }, // Auto-cleanup after 60s of inactivity
219
+ );
1897
220
  ```
1898
221
 
1899
- ### Suspense Integration
222
+ ### Usage in Components
1900
223
 
1901
- atomirx is designed to work seamlessly with React Suspense:
224
+ Use the `from` helper in `useSelector` (or `derived`/`effect`) to safely access pool atoms.
1902
225
 
1903
226
  ```tsx
1904
- import { Suspense } from "react";
1905
- import { ErrorBoundary } from "react-error-boundary";
1906
- import { atom } from "atomirx";
1907
- import { useSelector } from "atomirx/react";
1908
-
1909
- const user$ = atom(fetchUser());
227
+ function UserCard({ id }) {
228
+ // Safe: "from" ensures the atom is marked as used
229
+ const user = useSelector(({ read, from }) => {
230
+ const user$ = from(userPool, id);
231
+ return read(user$);
232
+ });
1910
233
 
1911
- function UserProfile() {
1912
- // Suspends until user$ resolves
1913
- const user = useSelector(user$);
1914
234
  return <div>{user.name}</div>;
1915
235
  }
1916
-
1917
- function App() {
1918
- return (
1919
- <ErrorBoundary fallback={<div>Something went wrong</div>}>
1920
- <Suspense fallback={<div>Loading...</div>}>
1921
- <UserProfile />
1922
- </Suspense>
1923
- </ErrorBoundary>
1924
- );
1925
- }
1926
- ```
1927
-
1928
- #### Nested Suspense Boundaries
1929
-
1930
- ```tsx
1931
- function Dashboard() {
1932
- return (
1933
- <div>
1934
- <Suspense fallback={<HeaderSkeleton />}>
1935
- <Header /> {/* Depends on user$ */}
1936
- </Suspense>
1937
-
1938
- <Suspense fallback={<FeedSkeleton />}>
1939
- <Feed /> {/* Depends on posts$ */}
1940
- </Suspense>
1941
-
1942
- <Suspense fallback={<SidebarSkeleton />}>
1943
- <Sidebar /> {/* Depends on recommendations$ */}
1944
- </Suspense>
1945
- </div>
1946
- );
1947
- }
1948
- ```
1949
-
1950
- ## API Reference
1951
-
1952
- ### Core API
1953
-
1954
- #### `atom<T>(initialValue, options?)`
1955
-
1956
- Creates a mutable reactive atom.
1957
-
1958
- ```typescript
1959
- function atom<T>(
1960
- initialValue: T | Promise<T> | (() => T | Promise<T>),
1961
- options?: { fallback?: T }
1962
- ): MutableAtom<T>;
1963
- ```
1964
-
1965
- #### `derived<T>(selector)`
1966
-
1967
- Creates a read-only derived atom.
1968
-
1969
- ```typescript
1970
- function derived<T>(selector: (context: SelectContext) => T): Atom<T>;
1971
-
1972
- // Legacy array API (backward compatible)
1973
- function derived<T, S>(source: Atom<S>, selector: (get: () => S) => T): Atom<T>;
1974
-
1975
- function derived<T, S extends readonly Atom<any>[]>(
1976
- sources: S,
1977
- selector: (...getters: GetterTuple<S>) => T
1978
- ): Atom<T>;
1979
- ```
1980
-
1981
- #### `effect(fn, options?)`
1982
-
1983
- Creates a side effect that runs when dependencies change.
1984
-
1985
- ```typescript
1986
- interface EffectContext extends SelectContext {
1987
- onCleanup: (cleanup: VoidFunction) => void;
1988
- onError: (handler: (error: unknown) => void) => void;
1989
- }
1990
-
1991
- function effect(
1992
- fn: (context: EffectContext) => void,
1993
- options?: { onError?: (error: Error) => void }
1994
- ): () => void; // Returns dispose function
1995
- ```
1996
-
1997
- #### `batch(fn)`
1998
-
1999
- Batches multiple updates into a single notification.
2000
-
2001
- ```typescript
2002
- function batch<T>(fn: () => T): T;
2003
- ```
2004
-
2005
- #### `emitter<T>()`
2006
-
2007
- Creates a pub/sub event emitter.
2008
-
2009
- ```typescript
2010
- function emitter<T>(): {
2011
- on: (listener: (value: T) => void) => () => void;
2012
- emit: (value: T) => void;
2013
- settle: (value: T) => void;
2014
- };
2015
- ```
2016
-
2017
- #### `define<T>(factory, options?)`
2018
-
2019
- Creates a swappable lazy singleton.
2020
-
2021
- ```typescript
2022
- function define<T>(
2023
- factory: () => T,
2024
- options?: { eager?: boolean }
2025
- ): {
2026
- (): T;
2027
- override: (factory: () => T) => void;
2028
- reset: () => void;
2029
- };
2030
- ```
2031
-
2032
- #### `isAtom(value)`
2033
-
2034
- Type guard for atoms.
2035
-
2036
- ```typescript
2037
- function isAtom<T>(value: unknown): value is Atom<T>;
2038
- ```
2039
-
2040
- ### SelectContext API
2041
-
2042
- Available in `derived()`, `effect()`, `useSelector()`, and `rx()`:
2043
-
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 |
2054
-
2055
- **Behavior:**
2056
-
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)
2059
- - `all()`: Suspends until all atoms are ready, throws on first error
2060
- - `any()`: Returns first ready value, throws AggregateError if all error
2061
- - `race()`: Returns first settled (ready or error)
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
236
  ```
2120
237
 
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 }` |
238
+ ---
2127
239
 
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).
240
+ ## DevTools
2129
241
 
2130
- ### React API
242
+ Atomirx comes with a powerful DevTools suite for debugging.
2131
243
 
2132
- #### `useSelector`
244
+ ### 1. Setup (App Entry)
2133
245
 
2134
- ```typescript
2135
- // Shorthand
2136
- function useSelector<T>(atom: Atom<T>): T;
246
+ Initialize the devtools registry in your app's entry point (e.g., `main.tsx` or `index.ts`).
2137
247
 
2138
- // Context selector
2139
- function useSelector<T>(
2140
- selector: (context: SelectContext) => T,
2141
- equals?: (prev: T, next: T) => boolean
2142
- ): T;
2143
- ```
248
+ ```ts
249
+ import { setupDevtools } from "atomirx/devtools";
2144
250
 
2145
- #### `rx`
2146
-
2147
- ```typescript
2148
- // Shorthand
2149
- function rx<T>(atom: Atom<T>): ReactNode;
2150
- function rx<T>(atom: Atom<T>, options?: RxOptions<T>): ReactNode;
2151
-
2152
- // Context selector
2153
- function rx<T>(selector: (context: SelectContext) => T): ReactNode;
2154
- function rx<T>(
2155
- selector: (context: SelectContext) => T,
2156
- options?: Equality<T> | RxOptions<T>
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[];
251
+ // Enable devtools tracking
252
+ if (process.env.NODE_ENV === "development") {
253
+ setupDevtools();
2168
254
  }
2169
255
  ```
2170
256
 
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
- ```
257
+ ### 2. React UI Component
2186
258
 
2187
- **With inline loading/error handlers:**
259
+ Add the `<DevToolsPanel />` to your root component.
2188
260
 
2189
261
  ```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
- }
262
+ import { DevToolsPanel } from "atomirx/react-devtools";
2203
263
 
2204
- // Both handlers
2205
- {
2206
- rx(asyncAtom$, {
2207
- loading: () => <Skeleton />,
2208
- error: ({ error }) => <ErrorMessage error={error} />,
2209
- });
2210
- }
264
+ function App() {
265
+ return (
266
+ <>
267
+ <YourApp />
2211
268
 
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
- });
269
+ {/* Renders a floating dockable panel */}
270
+ {process.env.NODE_ENV === "development" && (
271
+ <DevToolsPanel defaultPosition="bottom-right" />
272
+ )}
273
+ </>
274
+ );
2219
275
  }
2220
276
  ```
2221
277
 
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
-
2231
- #### `useAction`
2232
-
2233
- ```typescript
2234
- function useAction<T, Args extends any[]>(
2235
- action: (context: { signal: AbortSignal }, ...args: Args) => Promise<T>,
2236
- options?: {
2237
- lazy?: boolean;
2238
- deps?: (Atom<any> | any)[];
2239
- }
2240
- ): {
2241
- (...args: Args): void;
2242
- status: "idle" | "loading" | "success" | "error";
2243
- result: T | undefined;
2244
- error: unknown;
2245
- abort: () => void;
2246
- reset: () => void;
2247
- };
2248
- ```
278
+ ---
2249
279
 
2250
- #### `useStable`
280
+ ## API Reference
2251
281
 
2252
- ```typescript
2253
- function useStable<T extends Record<string, any>>(
2254
- input: T,
2255
- equals?: (prev: T[keyof T], next: T[keyof T]) => boolean
2256
- ): T;
2257
- ```
282
+ ### Core (`atomirx`)
2258
283
 
2259
- ## TypeScript Integration
284
+ - `atom(initialValue, options?)`: Create a mutable atom.
285
+ - `derived(calculator, options?)`: Create a computed atom.
286
+ - `effect(runner, options?)`: Run side effects.
287
+ - `pool(factory, options)`: Create a garbage-collected atom pool.
288
+ - `batch(fn)`: Batch multiple updates.
2260
289
 
2261
- atomirx is written in TypeScript and provides full type inference:
290
+ ### React (`atomirx/react`)
2262
291
 
2263
- ```typescript
2264
- import { atom, derived } from "atomirx";
292
+ - `useSelector(atom | selector)`: Subscribe to state.
293
+ - `rx(atom | selector)`: Renderless component for fine-grained updates.
2265
294
 
2266
- // Types are automatically inferred
2267
- const count$ = atom(0); // MutableAtom<number>
2268
- const name$ = atom("John"); // MutableAtom<string>
2269
- const doubled$ = derived(({ read }) => read(count$) * 2); // Atom<number>
295
+ ### Select Context (inside `derived`, `effect`, `useSelector`)
2270
296
 
2271
- // Explicit typing when needed
2272
- interface User {
2273
- id: string;
2274
- name: string;
2275
- email: string;
2276
- }
297
+ - `read(atom)`: Get value and subscribe.
298
+ - `from(pool, params)`: Get atom from pool safely.
299
+ - `all([atoms])`: Wait for all promises.
300
+ - `race({ key: atom })`: Race promises.
301
+ - `state(atom)`: Get `{ status, value, error }` without suspending.
2277
302
 
2278
- const user$ = atom<User | null>(null); // MutableAtom<User | null>
2279
- const userData$ = atom<User>(fetchUser()); // MutableAtom<User>
303
+ ---
2280
304
 
2281
- // Type-safe selectors
2282
- const userName$ = derived(({ read }) => {
2283
- const user = read(user$);
2284
- return user?.name ?? "Anonymous"; // Atom<string>
2285
- });
305
+ ## CLI
2286
306
 
2287
- // Generic atoms
2288
- function createListAtom<T>(initial: T[] = []) {
2289
- const items$ = atom<T[]>(initial);
307
+ ### Install AI Skills (Cursor IDE)
2290
308
 
2291
- return {
2292
- items$,
2293
- add: (item: T) => items$.set((list) => [...list, item]),
2294
- remove: (predicate: (item: T) => boolean) =>
2295
- items$.set((list) => list.filter((item) => !predicate(item))),
2296
- };
2297
- }
309
+ atomirx includes AI skills for Cursor IDE to help you write better code with AI assistance.
2298
310
 
2299
- const todoList = createListAtom<Todo>();
311
+ ```bash
312
+ npx atomirx add-skill
2300
313
  ```
2301
314
 
2302
- ## Comparison with Other Libraries
2303
-
2304
- | Feature | atomirx | Redux Toolkit | Zustand | Jotai | Recoil |
2305
- | -------------------- | ----------- | ------------- | ------- | ----------- | -------- |
2306
- | Bundle size | ~3KB | ~12KB | ~3KB | ~8KB | ~20KB |
2307
- | Boilerplate | Minimal | Low | Minimal | Minimal | Medium |
2308
- | TypeScript | First-class | First-class | Good | First-class | Good |
2309
- | Async support | Built-in | RTK Query | Manual | Built-in | Built-in |
2310
- | Fine-grained updates | Yes | No | Partial | Yes | Yes |
2311
- | Suspense support | Native | No | No | Yes | Yes |
2312
- | DevTools | Planned | Yes | Yes | Yes | Yes |
2313
- | Learning curve | Low | Medium | Low | Low | Medium |
2314
-
2315
- ### When to Use atomirx
2316
-
2317
- **Choose atomirx when you want:**
2318
-
2319
- - Minimal API with maximum capability
2320
- - First-class async support without additional packages
2321
- - Fine-grained reactivity for optimal performance
2322
- - React Suspense integration out of the box
2323
- - Strong TypeScript inference
2324
-
2325
- **Consider alternatives when you need:**
2326
-
2327
- - Time-travel debugging (Redux Toolkit)
2328
- - Established ecosystem with many plugins (Redux)
2329
- - Server-side state management (TanStack Query)
2330
-
2331
- ## Resources & Learning
2332
-
2333
- ### Documentation
2334
-
2335
- - [API Reference](#api-reference) - Complete API documentation
2336
- - [Usage Guide](#usage-guide) - In-depth usage patterns
2337
- - [React Integration](#react-integration) - React-specific features
2338
-
2339
- ### Examples
2340
-
2341
- - [Counter](./examples/counter) - Basic counter example
2342
- - [Todo App](./examples/todo) - Full todo application
2343
- - [Async Data](./examples/async) - Async data fetching patterns
2344
- - [Testing](./examples/testing) - Testing strategies with `define()`
315
+ This installs atomirx best practices, patterns, and API documentation to `.cursor/skills/atomirx/`, enabling your AI assistant to understand atomirx conventions.
2345
316
 
2346
- ### Community
317
+ **What's included:**
318
+ - Core patterns (atoms, derived, effects, pools)
319
+ - React integration (useSelector, rx, useAction, useStable)
320
+ - Service/Store architecture templates
321
+ - Testing patterns
322
+ - Error handling with `safe()`
2347
323
 
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
324
+ ---
2350
325
 
2351
- ## License
326
+ ## Links
2352
327
 
2353
- MIT License
328
+ - [GitHub](https://github.com/linq2js/atomirx) - Source code & issues
329
+ - [Changelog](https://github.com/linq2js/atomirx/releases) - Release notes
2354
330
 
2355
- Copyright (c) atomirx contributors
331
+ ---
2356
332
 
2357
- Permission is hereby granted, free of charge, to any person obtaining a copy
2358
- of this software and associated documentation files (the "Software"), to deal
2359
- in the Software without restriction, including without limitation the rights
2360
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
2361
- copies of the Software, and to permit persons to whom the Software is
2362
- furnished to do so, subject to the following conditions:
333
+ <div align="center">
2363
334
 
2364
- The above copyright notice and this permission notice shall be included in all
2365
- copies or substantial portions of the Software.
335
+ **MIT License**
2366
336
 
2367
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
2368
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
2369
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
2370
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
2371
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2372
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2373
- SOFTWARE.
337
+ </div>