atomirx 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/README.md +1666 -0
  2. package/coverage/base.css +224 -0
  3. package/coverage/block-navigation.js +87 -0
  4. package/coverage/clover.xml +1440 -0
  5. package/coverage/coverage-final.json +14 -0
  6. package/coverage/favicon.png +0 -0
  7. package/coverage/index.html +131 -0
  8. package/coverage/prettify.css +1 -0
  9. package/coverage/prettify.js +2 -0
  10. package/coverage/sort-arrow-sprite.png +0 -0
  11. package/coverage/sorter.js +210 -0
  12. package/coverage/src/core/atom.ts.html +889 -0
  13. package/coverage/src/core/batch.ts.html +223 -0
  14. package/coverage/src/core/define.ts.html +805 -0
  15. package/coverage/src/core/emitter.ts.html +919 -0
  16. package/coverage/src/core/equality.ts.html +631 -0
  17. package/coverage/src/core/hook.ts.html +460 -0
  18. package/coverage/src/core/index.html +281 -0
  19. package/coverage/src/core/isAtom.ts.html +100 -0
  20. package/coverage/src/core/isPromiseLike.ts.html +133 -0
  21. package/coverage/src/core/onCreateHook.ts.html +136 -0
  22. package/coverage/src/core/scheduleNotifyHook.ts.html +94 -0
  23. package/coverage/src/core/types.ts.html +523 -0
  24. package/coverage/src/core/withUse.ts.html +253 -0
  25. package/coverage/src/index.html +116 -0
  26. package/coverage/src/index.ts.html +106 -0
  27. package/dist/core/atom.d.ts +63 -0
  28. package/dist/core/atom.test.d.ts +1 -0
  29. package/dist/core/atomState.d.ts +104 -0
  30. package/dist/core/atomState.test.d.ts +1 -0
  31. package/dist/core/batch.d.ts +126 -0
  32. package/dist/core/batch.test.d.ts +1 -0
  33. package/dist/core/define.d.ts +173 -0
  34. package/dist/core/define.test.d.ts +1 -0
  35. package/dist/core/derived.d.ts +102 -0
  36. package/dist/core/derived.test.d.ts +1 -0
  37. package/dist/core/effect.d.ts +120 -0
  38. package/dist/core/effect.test.d.ts +1 -0
  39. package/dist/core/emitter.d.ts +237 -0
  40. package/dist/core/emitter.test.d.ts +1 -0
  41. package/dist/core/equality.d.ts +62 -0
  42. package/dist/core/equality.test.d.ts +1 -0
  43. package/dist/core/hook.d.ts +134 -0
  44. package/dist/core/hook.test.d.ts +1 -0
  45. package/dist/core/isAtom.d.ts +9 -0
  46. package/dist/core/isPromiseLike.d.ts +9 -0
  47. package/dist/core/isPromiseLike.test.d.ts +1 -0
  48. package/dist/core/onCreateHook.d.ts +79 -0
  49. package/dist/core/promiseCache.d.ts +134 -0
  50. package/dist/core/promiseCache.test.d.ts +1 -0
  51. package/dist/core/scheduleNotifyHook.d.ts +51 -0
  52. package/dist/core/select.d.ts +151 -0
  53. package/dist/core/selector.test.d.ts +1 -0
  54. package/dist/core/types.d.ts +279 -0
  55. package/dist/core/withUse.d.ts +38 -0
  56. package/dist/core/withUse.test.d.ts +1 -0
  57. package/dist/index-2ok7ilik.js +1217 -0
  58. package/dist/index-B_5SFzfl.cjs +1 -0
  59. package/dist/index.cjs +1 -0
  60. package/dist/index.d.ts +14 -0
  61. package/dist/index.js +20 -0
  62. package/dist/index.test.d.ts +1 -0
  63. package/dist/react/index.cjs +30 -0
  64. package/dist/react/index.d.ts +7 -0
  65. package/dist/react/index.js +823 -0
  66. package/dist/react/rx.d.ts +250 -0
  67. package/dist/react/rx.test.d.ts +1 -0
  68. package/dist/react/strictModeTest.d.ts +10 -0
  69. package/dist/react/useAction.d.ts +381 -0
  70. package/dist/react/useAction.test.d.ts +1 -0
  71. package/dist/react/useStable.d.ts +183 -0
  72. package/dist/react/useStable.test.d.ts +1 -0
  73. package/dist/react/useValue.d.ts +134 -0
  74. package/dist/react/useValue.test.d.ts +1 -0
  75. package/package.json +57 -0
  76. package/scripts/publish.js +198 -0
  77. package/src/core/atom.test.ts +369 -0
  78. package/src/core/atom.ts +189 -0
  79. package/src/core/atomState.test.ts +342 -0
  80. package/src/core/atomState.ts +256 -0
  81. package/src/core/batch.test.ts +257 -0
  82. package/src/core/batch.ts +172 -0
  83. package/src/core/define.test.ts +342 -0
  84. package/src/core/define.ts +243 -0
  85. package/src/core/derived.test.ts +381 -0
  86. package/src/core/derived.ts +339 -0
  87. package/src/core/effect.test.ts +196 -0
  88. package/src/core/effect.ts +184 -0
  89. package/src/core/emitter.test.ts +364 -0
  90. package/src/core/emitter.ts +392 -0
  91. package/src/core/equality.test.ts +392 -0
  92. package/src/core/equality.ts +182 -0
  93. package/src/core/hook.test.ts +227 -0
  94. package/src/core/hook.ts +177 -0
  95. package/src/core/isAtom.ts +27 -0
  96. package/src/core/isPromiseLike.test.ts +72 -0
  97. package/src/core/isPromiseLike.ts +16 -0
  98. package/src/core/onCreateHook.ts +92 -0
  99. package/src/core/promiseCache.test.ts +239 -0
  100. package/src/core/promiseCache.ts +279 -0
  101. package/src/core/scheduleNotifyHook.ts +53 -0
  102. package/src/core/select.ts +454 -0
  103. package/src/core/selector.test.ts +257 -0
  104. package/src/core/types.ts +311 -0
  105. package/src/core/withUse.test.ts +249 -0
  106. package/src/core/withUse.ts +56 -0
  107. package/src/index.test.ts +80 -0
  108. package/src/index.ts +51 -0
  109. package/src/react/index.ts +20 -0
  110. package/src/react/rx.test.tsx +416 -0
  111. package/src/react/rx.tsx +300 -0
  112. package/src/react/strictModeTest.tsx +71 -0
  113. package/src/react/useAction.test.ts +989 -0
  114. package/src/react/useAction.ts +605 -0
  115. package/src/react/useStable.test.ts +553 -0
  116. package/src/react/useStable.ts +288 -0
  117. package/src/react/useValue.test.ts +182 -0
  118. package/src/react/useValue.ts +261 -0
  119. package/tsconfig.json +9 -0
  120. package/v2.md +725 -0
  121. package/vite.config.ts +39 -0
package/README.md ADDED
@@ -0,0 +1,1666 @@
1
+ # Atomirx
2
+
3
+ **The Official, Opinionated, Batteries-Included Reactive State Management for TypeScript**
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
+ - [Using Create React App](#using-create-react-app)
28
+ - [Adding to an Existing Project](#adding-to-an-existing-project)
29
+ - [Why atomirx?](#why-atomirx)
30
+ - [The Problem](#the-problem)
31
+ - [The Solution](#the-solution)
32
+ - [Design Philosophy](#design-philosophy)
33
+ - [What's Included](#whats-included)
34
+ - [Core](#core)
35
+ - [React Bindings (`atomirx/react`)](#react-bindings-atomirxreact)
36
+ - [Getting Started](#getting-started)
37
+ - [Basic Example: Counter](#basic-example-counter)
38
+ - [React Example: Todo App](#react-example-todo-app)
39
+ - [Code Conventions](#code-conventions)
40
+ - [Naming: The `$` Suffix](#naming-the--suffix)
41
+ - [When to Use Each Primitive](#when-to-use-each-primitive)
42
+ - [Atom Storage: Stable Scopes Only](#atom-storage-stable-scopes-only)
43
+ - [Complete Example: Todo App with Async](#complete-example-todo-app-with-async)
44
+ - [Usage Guide](#usage-guide)
45
+ - [Atoms: The Foundation](#atoms-the-foundation)
46
+ - [Creating Atoms](#creating-atoms)
47
+ - [Reading Atom Values](#reading-atom-values)
48
+ - [Updating Atoms](#updating-atoms)
49
+ - [Subscribing to Changes](#subscribing-to-changes)
50
+ - [Complete Atom API](#complete-atom-api)
51
+ - [Derived State: Computed Values](#derived-state-computed-values)
52
+ - [Basic Derived State](#basic-derived-state)
53
+ - [Conditional Dependencies](#conditional-dependencies)
54
+ - [Suspense-Style Getters](#suspense-style-getters)
55
+ - [Derived from Multiple Async Sources](#derived-from-multiple-async-sources)
56
+ - [Effects: Side Effect Management](#effects-side-effect-management)
57
+ - [Basic Effects](#basic-effects)
58
+ - [Effects with Cleanup](#effects-with-cleanup)
59
+ - [Effects with Error Handling](#effects-with-error-handling)
60
+ - [Effects with Multiple Dependencies](#effects-with-multiple-dependencies)
61
+ - [Async Patterns](#async-patterns)
62
+ - [`all()` - Wait for All (like Promise.all)](#all---wait-for-all-like-promiseall)
63
+ - [`any()` - First Ready (like Promise.any)](#any---first-ready-like-promiseany)
64
+ - [`race()` - First Settled (like Promise.race)](#race---first-settled-like-promiserace)
65
+ - [`settled()` - All Results (like Promise.allSettled)](#settled---all-results-like-promiseallsettled)
66
+ - [Async Utility Summary](#async-utility-summary)
67
+ - [Batching Updates](#batching-updates)
68
+ - [Event System](#event-system)
69
+ - [Dependency Injection](#dependency-injection)
70
+ - [Atom Metadata and Middleware](#atom-metadata-and-middleware)
71
+ - [Extending AtomMeta with TypeScript](#extending-atommeta-with-typescript)
72
+ - [Using onCreateHook for Middleware](#using-oncreatehook-for-middleware)
73
+ - [Creating Persisted Atoms](#creating-persisted-atoms)
74
+ - [Multiple Middleware Example](#multiple-middleware-example)
75
+ - [Hook Info Types](#hook-info-types)
76
+ - [React Integration](#react-integration)
77
+ - [useValue Hook](#usevalue-hook)
78
+ - [Custom Equality](#custom-equality)
79
+ - [Reactive Components with rx](#reactive-components-with-rx)
80
+ - [Async Actions with useAction](#async-actions-with-useaction)
81
+ - [Eager Execution](#eager-execution)
82
+ - [useAction API](#useaction-api)
83
+ - [Reference Stability with useStable](#reference-stability-with-usestable)
84
+ - [Suspense Integration](#suspense-integration)
85
+ - [Nested Suspense Boundaries](#nested-suspense-boundaries)
86
+ - [API Reference](#api-reference)
87
+ - [Core API](#core-api)
88
+ - [`atom<T>(initialValue, options?)`](#atomtinitialvalue-options)
89
+ - [`derived<T>(selector)`](#derivedtselector)
90
+ - [`effect(fn, options?)`](#effectfn-options)
91
+ - [`batch(fn)`](#batchfn)
92
+ - [`emitter<T>()`](#emittert)
93
+ - [`define<T>(factory, options?)`](#definetfactory-options)
94
+ - [`isAtom(value)`](#isatomvalue)
95
+ - [SelectContext API](#selectcontext-api)
96
+ - [React API](#react-api)
97
+ - [`useValue`](#usevalue)
98
+ - [`rx`](#rx)
99
+ - [`useAction`](#useaction)
100
+ - [`useStable`](#usestable)
101
+ - [TypeScript Integration](#typescript-integration)
102
+ - [Comparison with Other Libraries](#comparison-with-other-libraries)
103
+ - [When to Use atomirx](#when-to-use-atomirx)
104
+ - [Resources \& Learning](#resources--learning)
105
+ - [Documentation](#documentation)
106
+ - [Examples](#examples)
107
+ - [Community](#community)
108
+ - [License](#license)
109
+
110
+ ## Installation
111
+
112
+ ### Using Create React App
113
+
114
+ The fastest way to get started is using our official template:
115
+
116
+ ```bash
117
+ npx create-react-app my-app --template atomirx
118
+ ```
119
+
120
+ ### Adding to an Existing Project
121
+
122
+ atomirx is available as a package on NPM for use with a module bundler or in a Node application:
123
+
124
+ ```bash
125
+ # NPM
126
+ npm install atomirx
127
+
128
+ # Yarn
129
+ yarn add atomirx
130
+
131
+ # PNPM
132
+ pnpm add atomirx
133
+ ```
134
+
135
+ The package includes precompiled ESM and CommonJS builds, along with TypeScript type definitions.
136
+
137
+ ## Why atomirx?
138
+
139
+ ### The Problem
140
+
141
+ Traditional state management solutions often require:
142
+
143
+ - **Excessive boilerplate** for simple state updates
144
+ - **Separate packages** for async operations, caching, and derived state
145
+ - **Manual subscription management** leading to memory leaks
146
+ - **Coarse-grained updates** causing unnecessary re-renders
147
+ - **Complex mental models** for understanding data flow
148
+
149
+ ### The Solution
150
+
151
+ atomirx provides a **unified, minimal API** that handles all common state management patterns out of the box:
152
+
153
+ | Challenge | atomirx Solution |
154
+ | ----------------- | -------------------------------------------------------------- |
155
+ | Mutable state | `atom()` - single source of truth with automatic subscriptions |
156
+ | Computed values | `derived()` - automatic dependency tracking and memoization |
157
+ | Side effects | `effect()` - declarative effects with cleanup |
158
+ | Async operations | Built-in Promise support with loading/error states |
159
+ | React integration | Suspense-compatible hooks with fine-grained updates |
160
+ | Testing | `define()` - dependency injection with easy mocking |
161
+
162
+ ### Design Philosophy
163
+
164
+ atomirx is built on these core principles:
165
+
166
+ 1. **Minimal API Surface** - Learn three functions (`atom`, `derived`, `effect`) and you're productive
167
+ 2. **Async-First** - Promises are first-class citizens, not an afterthought
168
+ 3. **Fine-Grained Reactivity** - Only the code that needs to run, runs
169
+ 4. **Type Safety** - Full TypeScript inference without manual type annotations
170
+ 5. **Framework Agnostic** - Core library has zero dependencies; React bindings are optional
171
+ 6. **Suspense-Native** - Designed from the ground up for React Suspense patterns
172
+
173
+ ## What's Included
174
+
175
+ atomirx includes these APIs:
176
+
177
+ ### Core
178
+
179
+ - **`atom()`**: Creates mutable reactive state containers with built-in async support
180
+ - **`derived()`**: Creates computed values with automatic dependency tracking
181
+ - **`effect()`**: Runs side effects that automatically re-execute when dependencies change
182
+ - **`batch()`**: Groups multiple updates into a single notification cycle
183
+ - **`define()`**: Creates swappable lazy singletons for dependency injection
184
+
185
+ ### React Bindings (`atomirx/react`)
186
+
187
+ - **`useValue()`**: Subscribe to atoms with automatic re-rendering
188
+ - **`rx()`**: Inline reactive components for fine-grained updates
189
+ - **`useAction()`**: Handle async operations with loading/error states
190
+ - **`useStable()`**: Stabilize object/array/callback references
191
+
192
+ ## Getting Started
193
+
194
+ ### Basic Example: Counter
195
+
196
+ ```typescript
197
+ import { atom, derived, effect } from "atomirx";
198
+
199
+ // Step 1: Create an atom (mutable state)
200
+ const count$ = atom(0);
201
+
202
+ // Step 2: Create derived state (computed values)
203
+ const doubled$ = derived(({ get }) => get(count$) * 2);
204
+ const message$ = derived(({ get }) => {
205
+ const count = get(count$);
206
+ return count === 0 ? "Click to start!" : `Count: ${count}`;
207
+ });
208
+
209
+ // Step 3: React to changes with effects
210
+ effect(({ get }) => {
211
+ console.log("Current count:", get(count$));
212
+ console.log("Doubled value:", get(doubled$));
213
+ });
214
+
215
+ // Step 4: Update state
216
+ count$.set(5); // Logs: Current count: 5, Doubled value: 10
217
+ count$.set((n) => n + 1); // Logs: Current count: 6, Doubled value: 12
218
+ ```
219
+
220
+ ### React Example: Todo App
221
+
222
+ ```tsx
223
+ import { atom, derived } from "atomirx";
224
+ import { useValue, rx } from "atomirx/react";
225
+
226
+ // Define your state
227
+ interface Todo {
228
+ id: number;
229
+ text: string;
230
+ completed: boolean;
231
+ }
232
+
233
+ const todos$ = atom<Todo[]>([]);
234
+ const filter$ = atom<"all" | "active" | "completed">("all");
235
+
236
+ // Derive computed state
237
+ const filteredTodos$ = derived(({ get }) => {
238
+ const todos = get(todos$);
239
+ const filter = get(filter$);
240
+
241
+ switch (filter) {
242
+ case "active":
243
+ return todos.filter((t) => !t.completed);
244
+ case "completed":
245
+ return todos.filter((t) => t.completed);
246
+ default:
247
+ return todos;
248
+ }
249
+ });
250
+
251
+ const stats$ = derived(({ get }) => {
252
+ const todos = get(todos$);
253
+ return {
254
+ total: todos.length,
255
+ completed: todos.filter((t) => t.completed).length,
256
+ remaining: todos.filter((t) => !t.completed).length,
257
+ };
258
+ });
259
+
260
+ // Actions
261
+ const addTodo = (text: string) => {
262
+ todos$.set((todos) => [...todos, { id: Date.now(), text, completed: false }]);
263
+ };
264
+
265
+ const toggleTodo = (id: number) => {
266
+ todos$.set((todos) =>
267
+ todos.map((t) => (t.id === id ? { ...t, completed: !t.completed } : t))
268
+ );
269
+ };
270
+
271
+ // Components
272
+ function TodoList() {
273
+ const todos = useValue(filteredTodos$);
274
+
275
+ return (
276
+ <ul>
277
+ {todos.map((todo) => (
278
+ <li key={todo.id} onClick={() => toggleTodo(todo.id)}>
279
+ {todo.completed ? "✓" : "○"} {todo.text}
280
+ </li>
281
+ ))}
282
+ </ul>
283
+ );
284
+ }
285
+
286
+ function Stats() {
287
+ // Fine-grained updates: only re-renders when stats change
288
+ return (
289
+ <footer>
290
+ {rx(({ get }) => {
291
+ const { total, completed, remaining } = get(stats$);
292
+ return (
293
+ <span>
294
+ {remaining} of {total} remaining
295
+ </span>
296
+ );
297
+ })}
298
+ </footer>
299
+ );
300
+ }
301
+ ```
302
+
303
+ ## Code Conventions
304
+
305
+ Following consistent conventions makes atomirx code more readable and maintainable across your team.
306
+
307
+ ### Naming: The `$` Suffix
308
+
309
+ All atoms (both `atom()` and `derived()`) should use the `$` suffix. This convention:
310
+
311
+ - Clearly distinguishes reactive state from regular variables
312
+ - Makes it obvious when you're working with atoms vs plain values
313
+ - Improves code readability at a glance
314
+
315
+ ```typescript
316
+ // ✅ Good - clear that these are atoms
317
+ const count$ = atom(0);
318
+ const user$ = atom<User | null>(null);
319
+ const filteredItems$ = derived(({ get }) => /* ... */);
320
+
321
+ // ❌ Avoid - unclear what's reactive
322
+ const count = atom(0);
323
+ const user = atom<User | null>(null);
324
+ ```
325
+
326
+ ### When to Use Each Primitive
327
+
328
+ | Primitive | Purpose | Use When |
329
+ | ----------- | ----------------------- | --------------------------------------------------------------------------- |
330
+ | `atom()` | Store values | You need mutable state (including Promises) |
331
+ | `derived()` | Compute reactive values | You need to transform or combine atom values |
332
+ | `effect()` | Trigger side effects | You need to react to atom changes (sync to external systems, logging, etc.) |
333
+
334
+ ### Atom Storage: Stable Scopes Only
335
+
336
+ **Never store atoms in component/local scope.** Atoms created inside React components (even with `useRef`) can lead to:
337
+
338
+ - **Memory leaks** - atoms aren't properly disposed when components unmount
339
+ - **Forgotten disposal** - easy to forget cleanup logic
340
+ - **Multiple instances** - each component render may create new atoms
341
+
342
+ ```typescript
343
+ // ❌ BAD - atoms in component scope
344
+ function TodoList() {
345
+ // These atoms are created per component instance!
346
+ const todos$ = useRef(atom(() => fetchTodos())).current;
347
+ const filter$ = useRef(atom("all")).current;
348
+ // Memory leak: atoms not disposed on unmount
349
+ }
350
+
351
+ // ✅ GOOD - atoms at module scope
352
+ const todos$ = atom(() => fetchTodos());
353
+ const filter$ = atom("all");
354
+
355
+ function TodoList() {
356
+ const todos = useValue(filteredTodos$);
357
+ // ...
358
+ }
359
+ ```
360
+
361
+ **Use `define()` to organize atoms into modules:**
362
+
363
+ ```typescript
364
+ // ✅ BEST - atoms organized in a module with define()
365
+ const TodoModule = define(() => {
366
+ // Atoms are created once, lazily
367
+ const todos$ = atom(() => fetchTodos(), { meta: { key: "todos" } });
368
+ const filter$ = atom<"all" | "active" | "completed">("all");
369
+
370
+ const filteredTodos$ = derived(({ get }) => {
371
+ const filter = get(filter$);
372
+ const todos = get(todos$);
373
+ return filter === "all" ? todos : todos.filter(/* ... */);
374
+ });
375
+
376
+ return {
377
+ todos$,
378
+ filter$,
379
+ filteredTodos$,
380
+ setFilter: (f: "all" | "active" | "completed") => filter$.set(f),
381
+ refetch: () => todos$.set(fetchTodos()),
382
+ reset: () => todos$.reset(),
383
+ };
384
+ });
385
+
386
+ // Usage in React
387
+ function TodoList() {
388
+ const { filteredTodos$, setFilter } = TodoModule();
389
+ const todos = useValue(filteredTodos$);
390
+ // ...
391
+ }
392
+ ```
393
+
394
+ Benefits of `define()`:
395
+
396
+ - **Lazy singleton** - module created on first access
397
+ - **Testable** - use `.override()` to inject mocks
398
+ - **Disposable** - use `.invalidate()` to clean up and recreate
399
+ - **Organized** - group related atoms and actions together
400
+
401
+ **`atom()`** - Store raw values, including Promises:
402
+
403
+ ```typescript
404
+ // Synchronous values
405
+ const filter$ = atom("all");
406
+ const count$ = atom(0);
407
+
408
+ // Async values - store the Promise directly
409
+ const todoList$ = atom(() => fetchAllTodos()); // Lazy fetch on creation
410
+ const userData$ = atom(fetchUser(userId)); // Fetch immediately
411
+ ```
412
+
413
+ **`derived()`** - Handle reactive/async transformations:
414
+
415
+ ```typescript
416
+ // derived() automatically unwraps Promises from atoms
417
+ const filteredTodoList$ = derived(({ get }) => {
418
+ const filter = get(filter$);
419
+ const todoList = get(todoList$); // Promise is unwrapped automatically!
420
+
421
+ switch (filter) {
422
+ case "active":
423
+ return todoList.filter((t) => !t.completed);
424
+ case "completed":
425
+ return todoList.filter((t) => t.completed);
426
+ default:
427
+ return todoList;
428
+ }
429
+ });
430
+ ```
431
+
432
+ **`effect()`** - Coordinate updates across multiple atoms:
433
+
434
+ ```typescript
435
+ // Sync local state to server when it changes
436
+ effect(({ get, onCleanup }) => {
437
+ const settings = get(settings$);
438
+
439
+ const controller = new AbortController();
440
+ saveSettingsToServer(settings, { signal: controller.signal });
441
+
442
+ onCleanup(() => controller.abort());
443
+ });
444
+
445
+ // Update multiple atoms based on another atom's change
446
+ effect(({ get }) => {
447
+ const user = get(currentUser$);
448
+
449
+ if (user) {
450
+ // Trigger fetches for user-specific data
451
+ userPosts$.set(fetchUserPosts(user.id));
452
+ userSettings$.set(fetchUserSettings(user.id));
453
+ }
454
+ });
455
+ ```
456
+
457
+ ### Complete Example: Todo App with Async
458
+
459
+ ```typescript
460
+ import { atom, derived } from "atomirx";
461
+ import { useValue, rx } from "atomirx/react";
462
+ import { Suspense } from "react";
463
+
464
+ // Atoms store values (including Promises)
465
+ const filter$ = atom<"all" | "active" | "completed">("all");
466
+ const todoList$ = atom(() => fetchAllTodos()); // Lazy init, re-runs on reset()
467
+
468
+ // Derived handles reactive transformations (auto-unwraps Promises)
469
+ const filteredTodoList$ = derived(({ get }) => {
470
+ const filter = get(filter$);
471
+ const todoList = get(todoList$); // This is the resolved value, not a Promise!
472
+
473
+ switch (filter) {
474
+ case "active": return todoList.filter(t => !t.completed);
475
+ case "completed": return todoList.filter(t => t.completed);
476
+ default: return todoList;
477
+ }
478
+ });
479
+
480
+ // In UI - useValue suspends until data is ready
481
+ function TodoList() {
482
+ const filteredTodoList = useValue(filteredTodoList$);
483
+
484
+ return (
485
+ <ul>
486
+ {filteredTodoList.map(todo => (
487
+ <li key={todo.id}>{todo.text}</li>
488
+ ))}
489
+ </ul>
490
+ );
491
+ }
492
+
493
+ // Or use rx() for inline reactive rendering
494
+ function App() {
495
+ return (
496
+ <Suspense fallback={<div>Loading todos...</div>}>
497
+ {rx(({ get }) =>
498
+ get(filteredTodoList$).map(todo => <Todo key={todo.id} todo={todo} />)
499
+ )}
500
+ </Suspense>
501
+ );
502
+ }
503
+
504
+ // Refetch todos
505
+ function RefreshButton() {
506
+ return (
507
+ <button onClick={() => todoList$.reset()}>
508
+ Refresh
509
+ </button>
510
+ );
511
+ }
512
+ ```
513
+
514
+ ## Usage Guide
515
+
516
+ ### Atoms: The Foundation
517
+
518
+ Atoms are the building blocks of atomirx. They hold mutable state and automatically notify subscribers when the value changes.
519
+
520
+ #### Creating Atoms
521
+
522
+ ```typescript
523
+ import { atom } from "atomirx";
524
+
525
+ // Synchronous atom with initial value
526
+ const count$ = atom(0);
527
+ const user$ = atom<User | null>(null);
528
+ const settings$ = atom({ theme: "dark", language: "en" });
529
+
530
+ // Async atom - automatically tracks loading/error states
531
+ const userData$ = atom(fetchUser(userId));
532
+
533
+ // Lazy initialization - computation deferred until first access
534
+ const expensive$ = atom(() => computeExpensiveValue());
535
+
536
+ // With fallback value during loading/error
537
+ const posts$ = atom(fetchPosts(), { fallback: [] });
538
+ ```
539
+
540
+ #### Reading Atom Values
541
+
542
+ ```typescript
543
+ import { getAtomState, isPending } from "atomirx";
544
+
545
+ // Direct access (outside reactive context)
546
+ console.log(count$.value); // Current value (T or Promise<T>)
547
+
548
+ // Check atom state
549
+ const state = getAtomState(userData$);
550
+ if (state.status === "loading") {
551
+ console.log("Loading...");
552
+ } else if (state.status === "error") {
553
+ console.log("Error:", state.error);
554
+ } else {
555
+ console.log("User:", state.value);
556
+ }
557
+
558
+ // Quick loading check
559
+ console.log(isPending(userData$.value)); // true while Promise is pending
560
+ ```
561
+
562
+ #### Updating Atoms
563
+
564
+ ```typescript
565
+ // Direct value
566
+ count$.set(10);
567
+
568
+ // Functional update (receives current value)
569
+ count$.set((current) => current + 1);
570
+
571
+ // Async update
572
+ userData$.set(fetchUser(newUserId));
573
+
574
+ // Reset to initial state
575
+ count$.reset();
576
+ ```
577
+
578
+ #### Subscribing to Changes
579
+
580
+ ```typescript
581
+ // Subscribe to changes
582
+ const unsubscribe = count$.on((newValue) => {
583
+ console.log("Count changed to:", newValue);
584
+ });
585
+
586
+ // Await async atoms
587
+ await userData$;
588
+ console.log("User loaded:", userData$.value);
589
+
590
+ // Unsubscribe when done
591
+ unsubscribe();
592
+ ```
593
+
594
+ #### Complete Atom API
595
+
596
+ **MutableAtom** (created by `atom()`):
597
+
598
+ | Property/Method | Type | Description |
599
+ | --------------- | ------------ | -------------------------------------------------- |
600
+ | `value` | `T` | Current value (may be a Promise for async atoms) |
601
+ | `set(value)` | `void` | Update with value, Promise, or updater function |
602
+ | `reset()` | `void` | Reset to initial value |
603
+ | `on(listener)` | `() => void` | Subscribe to changes, returns unsubscribe function |
604
+
605
+ **DerivedAtom** (created by `derived()`):
606
+
607
+ | Property/Method | Type | Description |
608
+ | --------------- | ---------------- | ---------------------------------------------- |
609
+ | `value` | `Promise<T>` | Always returns a Promise |
610
+ | `staleValue` | `T \| undefined` | Fallback or last resolved value during loading |
611
+ | `state()` | `AtomState<T>` | Current state (ready/error/loading) |
612
+ | `refresh()` | `void` | Re-run the computation |
613
+ | `on(listener)` | `() => void` | Subscribe to changes, returns unsubscribe |
614
+
615
+ **AtomState** (returned by `state()` or `getAtomState()`):
616
+
617
+ ```typescript
618
+ type AtomState<T> =
619
+ | { status: "ready"; value: T }
620
+ | { status: "error"; error: unknown }
621
+ | { status: "loading"; promise: Promise<T> };
622
+ ```
623
+
624
+ ### Derived State: Computed Values
625
+
626
+ Derived atoms automatically compute values based on other atoms. They track dependencies at runtime and only recompute when those specific dependencies change.
627
+
628
+ #### Basic Derived State
629
+
630
+ ```typescript
631
+ import { atom, derived } from "atomirx";
632
+
633
+ const firstName$ = atom("John");
634
+ const lastName$ = atom("Doe");
635
+
636
+ // Derived state with automatic dependency tracking
637
+ const fullName$ = derived(({ get }) => {
638
+ return `${get(firstName$)} ${get(lastName$)}`;
639
+ });
640
+
641
+ // Derived atoms always return Promise<T> for .value
642
+ await fullName$.value; // "John Doe"
643
+
644
+ // Or use staleValue for synchronous access (after first resolution)
645
+ fullName$.staleValue; // "John Doe" (or undefined before first resolution)
646
+
647
+ // Check state
648
+ fullName$.state(); // { status: "ready", value: "John Doe" }
649
+
650
+ firstName$.set("Jane");
651
+ await fullName$.value; // "Jane Doe"
652
+ ```
653
+
654
+ #### Conditional Dependencies
655
+
656
+ One of atomirx's most powerful features is **conditional dependency tracking**. Dependencies are tracked based on actual runtime access, not static analysis:
657
+
658
+ ```typescript
659
+ const showDetails$ = atom(false);
660
+ const summary$ = atom("Brief overview");
661
+ const details$ = atom("Detailed information...");
662
+
663
+ const content$ = derived(({ get }) => {
664
+ // Only tracks showDetails$ initially
665
+ if (get(showDetails$)) {
666
+ // details$ becomes a dependency only when showDetails$ is true
667
+ return get(details$);
668
+ }
669
+ return get(summary$);
670
+ });
671
+
672
+ // When showDetails$ is false:
673
+ // - Changes to details$ do NOT trigger recomputation
674
+ // - Only changes to showDetails$ or summary$ trigger recomputation
675
+ ```
676
+
677
+ #### Suspense-Style Getters
678
+
679
+ The `get()` function follows React Suspense semantics for async atoms:
680
+
681
+ | Atom State | `get()` Behavior |
682
+ | ---------- | ----------------------------------------------- |
683
+ | Loading | Throws the Promise (caught by derived/Suspense) |
684
+ | Error | Throws the error |
685
+ | Ready | Returns the value |
686
+
687
+ ```typescript
688
+ const user$ = atom(fetchUser());
689
+
690
+ const userName$ = derived(({ get }) => {
691
+ // Automatically handles loading/error states
692
+ const user = get(user$);
693
+ return user.name;
694
+ });
695
+
696
+ // Check state
697
+ const state = userName$.state();
698
+ if (state.status === "loading") {
699
+ console.log("Loading...");
700
+ } else if (state.status === "error") {
701
+ console.log("Error:", state.error);
702
+ } else {
703
+ console.log("User name:", state.value);
704
+ }
705
+
706
+ // Or use staleValue with a fallback
707
+ const userName = derived(({ get }) => get(user$).name, { fallback: "Guest" });
708
+ userName.staleValue; // "Guest" during loading, then actual name
709
+ ```
710
+
711
+ #### Derived from Multiple Async Sources
712
+
713
+ ```typescript
714
+ const user$ = atom(fetchUser());
715
+ const posts$ = atom(fetchPosts());
716
+
717
+ const dashboard$ = derived(({ get }) => {
718
+ const user = get(user$); // Suspends if loading
719
+ const posts = get(posts$); // Suspends if loading
720
+
721
+ return {
722
+ userName: user.name,
723
+ postCount: posts.length,
724
+ };
725
+ });
726
+
727
+ // Check state
728
+ const state = dashboard$.state();
729
+ // state.status is "loading" until BOTH user$ and posts$ resolve
730
+
731
+ // Or use all() for explicit parallel loading
732
+ const dashboard2$ = derived(({ all }) => {
733
+ const [user, posts] = all(user$, posts$);
734
+ return { userName: user.name, postCount: posts.length };
735
+ });
736
+ ```
737
+
738
+ ### Effects: Side Effect Management
739
+
740
+ Effects run side effects whenever their dependencies change. They use the same reactive context as `derived()`.
741
+
742
+ #### Basic Effects
743
+
744
+ ```typescript
745
+ import { atom, effect } from "atomirx";
746
+
747
+ const count$ = atom(0);
748
+
749
+ // Effect runs immediately and on every change
750
+ const dispose = effect(({ get }) => {
751
+ console.log("Count is now:", get(count$));
752
+ });
753
+
754
+ count$.set(5); // Logs: "Count is now: 5"
755
+
756
+ // Clean up when done
757
+ dispose();
758
+ ```
759
+
760
+ #### Effects with Cleanup
761
+
762
+ Use `onCleanup()` to register cleanup functions that run before the next execution or on dispose:
763
+
764
+ ```typescript
765
+ const interval$ = atom(1000);
766
+
767
+ const dispose = effect(({ get, onCleanup }) => {
768
+ const ms = get(interval$);
769
+ const id = setInterval(() => console.log("tick"), ms);
770
+
771
+ // Cleanup runs before next execution or on dispose
772
+ onCleanup(() => clearInterval(id));
773
+ });
774
+
775
+ interval$.set(500); // Clears old interval, starts new one
776
+ dispose(); // Clears interval completely
777
+ ```
778
+
779
+ #### Effects with Error Handling
780
+
781
+ Use `onError()` to handle errors within the effect:
782
+
783
+ ```typescript
784
+ const dispose = effect(({ get, onError }) => {
785
+ onError((e) => console.error("Effect failed:", e));
786
+
787
+ const data = get(dataAtom$);
788
+ riskyOperation(data);
789
+ });
790
+
791
+ // Or use options.onError for unhandled errors
792
+ const dispose2 = effect(
793
+ ({ get }) => {
794
+ riskyOperation(get(dataAtom$));
795
+ },
796
+ { onError: (e) => console.error("Unhandled:", e) }
797
+ );
798
+ ```
799
+
800
+ #### Effects with Multiple Dependencies
801
+
802
+ ```typescript
803
+ const user$ = atom<User | null>(null);
804
+ const settings$ = atom({ notifications: true });
805
+
806
+ effect(({ get }) => {
807
+ const user = get(user$);
808
+ const settings = get(settings$);
809
+
810
+ if (user && settings.notifications) {
811
+ analytics.identify(user.id);
812
+ notifications.subscribe(user.id);
813
+ }
814
+ });
815
+ ```
816
+
817
+ ### Async Patterns
818
+
819
+ atomirx provides powerful utilities for working with multiple async atoms through the `SelectContext`.
820
+
821
+ #### `all()` - Wait for All (like Promise.all)
822
+
823
+ ```typescript
824
+ const user$ = atom(fetchUser());
825
+ const posts$ = atom(fetchPosts());
826
+ const comments$ = atom(fetchComments());
827
+
828
+ const dashboard$ = derived(({ all }) => {
829
+ // Suspends until ALL atoms resolve (variadic args)
830
+ const [user, posts, comments] = all(user$, posts$, comments$);
831
+
832
+ return { user, posts, comments };
833
+ });
834
+ ```
835
+
836
+ #### `any()` - First Ready (like Promise.any)
837
+
838
+ ```typescript
839
+ const primaryApi$ = atom(fetchFromPrimary());
840
+ const fallbackApi$ = atom(fetchFromFallback());
841
+
842
+ const data$ = derived(({ any }) => {
843
+ // Returns first successfully resolved value (variadic args)
844
+ return any(primaryApi$, fallbackApi$);
845
+ });
846
+ ```
847
+
848
+ #### `race()` - First Settled (like Promise.race)
849
+
850
+ ```typescript
851
+ const cache$ = atom(checkCache());
852
+ const api$ = atom(fetchFromApi());
853
+
854
+ const data$ = derived(({ race }) => {
855
+ // Returns first settled (ready OR error) (variadic args)
856
+ return race(cache$, api$);
857
+ });
858
+ ```
859
+
860
+ #### `settled()` - All Results (like Promise.allSettled)
861
+
862
+ ```typescript
863
+ const user$ = atom(fetchUser());
864
+ const posts$ = atom(fetchPosts());
865
+
866
+ const results$ = derived(({ settled }) => {
867
+ // Returns status for each atom (variadic args)
868
+ const [userResult, postsResult] = settled(user$, posts$);
869
+
870
+ return {
871
+ user: userResult.status === "ready" ? userResult.value : null,
872
+ posts: postsResult.status === "ready" ? postsResult.value : [],
873
+ hasErrors: userResult.status === "error" || postsResult.status === "error",
874
+ };
875
+ });
876
+ ```
877
+
878
+ #### Async Utility Summary
879
+
880
+ | Utility | Input | Output | Behavior |
881
+ | ----------- | -------------- | ---------------------- | ---------------------------------- |
882
+ | `all()` | Variadic atoms | Array of values | Suspends until all ready |
883
+ | `any()` | Variadic atoms | First ready value | First to resolve wins |
884
+ | `race()` | Variadic atoms | First settled value | First to settle (ready/error) wins |
885
+ | `settled()` | Variadic atoms | Array of SettledResult | Suspends until all settled |
886
+
887
+ **SettledResult type:**
888
+
889
+ ```typescript
890
+ type SettledResult<T> =
891
+ | { status: "ready"; value: T }
892
+ | { status: "error"; error: unknown };
893
+ ```
894
+
895
+ ### Batching Updates
896
+
897
+ When updating multiple atoms, use `batch()` to combine notifications:
898
+
899
+ ```typescript
900
+ import { atom, batch } from "atomirx";
901
+
902
+ const firstName$ = atom("John");
903
+ const lastName$ = atom("Doe");
904
+ const age$ = atom(30);
905
+
906
+ // Without batch: 3 separate notifications
907
+ firstName$.set("Jane");
908
+ lastName$.set("Smith");
909
+ age$.set(25);
910
+
911
+ // With batch: 1 notification with final state
912
+ batch(() => {
913
+ firstName$.set("Jane");
914
+ lastName$.set("Smith");
915
+ age$.set(25);
916
+ });
917
+ ```
918
+
919
+ ### Event System
920
+
921
+ The `emitter()` function provides a lightweight pub/sub system:
922
+
923
+ ```typescript
924
+ import { emitter } from "atomirx";
925
+
926
+ // Create typed emitter
927
+ const userEvents = emitter<{ type: "login" | "logout"; userId: string }>();
928
+
929
+ // Subscribe
930
+ const unsubscribe = userEvents.on((event) => {
931
+ console.log(`User ${event.userId} ${event.type}`);
932
+ });
933
+
934
+ // Emit events
935
+ userEvents.emit({ type: "login", userId: "123" });
936
+
937
+ // Settle pattern - late subscribers receive the settled value
938
+ const appReady = emitter<Config>();
939
+ appReady.settle(config); // All current AND future subscribers receive config
940
+ ```
941
+
942
+ ### Dependency Injection
943
+
944
+ The `define()` function creates swappable lazy singletons, perfect for testing:
945
+
946
+ ```typescript
947
+ import { define, atom } from "atomirx";
948
+
949
+ // Define a store factory
950
+ const counterStore = define(() => {
951
+ const count$ = atom(0);
952
+
953
+ return {
954
+ count$,
955
+ increment: () => count$.set((c) => c + 1),
956
+ decrement: () => count$.set((c) => c - 1),
957
+ reset: () => count$.reset(),
958
+ };
959
+ });
960
+
961
+ // Usage - lazy singleton (same instance everywhere)
962
+ const store = counterStore();
963
+ store.increment();
964
+
965
+ // Testing - override the factory
966
+ counterStore.override(() => ({
967
+ count$: atom(999),
968
+ increment: vi.fn(),
969
+ decrement: vi.fn(),
970
+ reset: vi.fn(),
971
+ }));
972
+
973
+ // Reset to original implementation
974
+ counterStore.reset();
975
+ ```
976
+
977
+ ### Atom Metadata and Middleware
978
+
979
+ atomirx supports custom metadata on atoms via the `meta` option. Combined with `onCreateHook`, you can implement cross-cutting concerns like persistence, logging, or validation.
980
+
981
+ #### Extending AtomMeta with TypeScript
982
+
983
+ Use TypeScript's module augmentation to add custom properties to `AtomMeta`:
984
+
985
+ ```typescript
986
+ // Extend the meta interfaces with your custom properties
987
+ declare module "atomirx" {
988
+ // MutableAtomMeta - for atom() specific options
989
+ interface MutableAtomMeta {
990
+ /** Whether the atom should be persisted to localStorage */
991
+ persisted?: boolean;
992
+ /**
993
+ * Custom validation function.
994
+ * Return true to allow the update, false to reject it.
995
+ */
996
+ validate?: (value: unknown) => boolean;
997
+ /** Optional error handler for validation failures */
998
+ onValidationError?: (value: unknown, key?: string) => void;
999
+ }
1000
+
1001
+ // DerivedAtomMeta - for derived() specific options
1002
+ interface DerivedAtomMeta {
1003
+ /** Custom cache key for memoization */
1004
+ cacheKey?: string;
1005
+ }
1006
+
1007
+ // AtomMeta - base type, shared by both (key, etc.)
1008
+ // interface AtomMeta { ... }
1009
+ }
1010
+ ```
1011
+
1012
+ #### Using onCreateHook for Middleware
1013
+
1014
+ The `onCreateHook` fires whenever an atom or module is created. Use the reducer pattern to compose multiple middlewares:
1015
+
1016
+ ```typescript
1017
+ import { onCreateHook } from "atomirx";
1018
+
1019
+ // Persistence middleware
1020
+ onCreateHook.override((prev) => (info) => {
1021
+ // Call previous middleware first (composition)
1022
+ prev?.(info);
1023
+
1024
+ // Only handle mutable atoms with persisted flag
1025
+ if (info.type === "mutable" && info.meta?.persisted && info.meta?.key) {
1026
+ const storageKey = `my-app-${info.meta.key}`;
1027
+
1028
+ // Restore from localStorage on creation (if not dirty)
1029
+ if (!info.atom.dirty()) {
1030
+ const stored = localStorage.getItem(storageKey);
1031
+ if (stored) {
1032
+ try {
1033
+ info.atom.set(JSON.parse(stored));
1034
+ } catch {
1035
+ // Invalid JSON, ignore
1036
+ }
1037
+ }
1038
+ }
1039
+
1040
+ // Save to localStorage on every change
1041
+ info.atom.on(() => {
1042
+ localStorage.setItem(storageKey, JSON.stringify(info.atom.value));
1043
+ });
1044
+ }
1045
+ });
1046
+ ```
1047
+
1048
+ #### Creating Persisted Atoms
1049
+
1050
+ Now atoms with `persisted: true` automatically sync with localStorage:
1051
+
1052
+ ```typescript
1053
+ // This atom will persist across page reloads
1054
+ const settings$ = atom(
1055
+ { theme: "dark", language: "en" },
1056
+ { meta: { key: "settings", persisted: true } }
1057
+ );
1058
+
1059
+ // Changes are automatically saved
1060
+ settings$.set({ theme: "light", language: "en" });
1061
+
1062
+ // On next page load, value is restored from localStorage
1063
+ ```
1064
+
1065
+ #### Multiple Middleware Example
1066
+
1067
+ Compose multiple middlewares using the reducer pattern:
1068
+
1069
+ ```typescript
1070
+ // Logging middleware
1071
+ onCreateHook.override((prev) => (info) => {
1072
+ prev?.(info);
1073
+ console.log(
1074
+ `[atomirx] Created ${info.type}: ${info.meta?.key ?? "anonymous"}`
1075
+ );
1076
+ });
1077
+
1078
+ // Validation middleware - wraps set() to validate before applying
1079
+ onCreateHook.override((prev) => (info) => {
1080
+ prev?.(info);
1081
+
1082
+ if (info.type === "mutable" && info.meta?.validate) {
1083
+ const validate = info.meta.validate;
1084
+ const originalSet = info.atom.set.bind(info.atom);
1085
+
1086
+ // Wrap set() with validation
1087
+ info.atom.set = (valueOrReducer) => {
1088
+ // Resolve the next value (handle both direct value and reducer)
1089
+ const nextValue =
1090
+ typeof valueOrReducer === "function"
1091
+ ? (valueOrReducer as (prev: unknown) => unknown)(info.atom.value)
1092
+ : valueOrReducer;
1093
+
1094
+ // Validate before applying
1095
+ if (!validate(nextValue)) {
1096
+ console.warn(
1097
+ `[atomirx] Validation failed for ${info.meta?.key}`,
1098
+ nextValue
1099
+ );
1100
+ return; // Reject the update
1101
+ }
1102
+
1103
+ originalSet(valueOrReducer);
1104
+ };
1105
+ }
1106
+ });
1107
+
1108
+ // Usage: atom with validation
1109
+ const age$ = atom(25, {
1110
+ meta: {
1111
+ key: "age",
1112
+ validate: (value) =>
1113
+ typeof value === "number" && value >= 0 && value <= 150,
1114
+ },
1115
+ });
1116
+
1117
+ age$.set(30); // OK
1118
+ age$.set(-5); // Rejected: "Validation failed for age"
1119
+ age$.set(200); // Rejected: "Validation failed for age"
1120
+ ```
1121
+
1122
+ > **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.
1123
+
1124
+ ```typescript
1125
+ // DevTools middleware
1126
+ onCreateHook.override((prev) => (info) => {
1127
+ prev?.(info);
1128
+
1129
+ if (info.type === "mutable" || info.type === "derived") {
1130
+ // Register with your devtools
1131
+ window.__ATOMIRX_DEVTOOLS__?.register(info);
1132
+ }
1133
+ });
1134
+ ```
1135
+
1136
+ #### Hook Info Types
1137
+
1138
+ The `onCreateHook` receives different info objects based on what's being created:
1139
+
1140
+ ```typescript
1141
+ // Mutable atom
1142
+ interface MutableAtomCreateInfo {
1143
+ type: "mutable";
1144
+ key: string | undefined;
1145
+ meta: AtomMeta | undefined;
1146
+ atom: MutableAtom<unknown>;
1147
+ }
1148
+
1149
+ // Derived atom
1150
+ interface DerivedAtomCreateInfo {
1151
+ type: "derived";
1152
+ key: string | undefined;
1153
+ meta: AtomMeta | undefined;
1154
+ atom: DerivedAtom<unknown, boolean>;
1155
+ }
1156
+
1157
+ // Module (from define())
1158
+ interface ModuleCreateInfo {
1159
+ type: "module";
1160
+ key: string | undefined;
1161
+ meta: ModuleMeta | undefined;
1162
+ module: unknown;
1163
+ }
1164
+ ```
1165
+
1166
+ ## React Integration
1167
+
1168
+ atomirx provides first-class React integration through the `atomirx/react` package.
1169
+
1170
+ ### useValue Hook
1171
+
1172
+ Subscribe to atom values with automatic re-rendering:
1173
+
1174
+ ```tsx
1175
+ import { useValue } from "atomirx/react";
1176
+ import { atom } from "atomirx";
1177
+
1178
+ const count$ = atom(0);
1179
+ const user$ = atom<User | null>(null);
1180
+
1181
+ function Counter() {
1182
+ // Shorthand: pass atom directly
1183
+ const count = useValue(count$);
1184
+
1185
+ // Context selector: compute derived value
1186
+ const doubled = useValue(({ get }) => get(count$) * 2);
1187
+
1188
+ // Multiple atoms
1189
+ const display = useValue(({ get }) => {
1190
+ const count = get(count$);
1191
+ const user = get(user$);
1192
+ return user ? `${user.name}: ${count}` : `Anonymous: ${count}`;
1193
+ });
1194
+
1195
+ return <div>{display}</div>;
1196
+ }
1197
+ ```
1198
+
1199
+ #### Custom Equality
1200
+
1201
+ ```tsx
1202
+ // Only re-render when specific fields change
1203
+ const userName = useValue(
1204
+ ({ get }) => get(user$)?.name,
1205
+ (prev, next) => prev === next
1206
+ );
1207
+ ```
1208
+
1209
+ ### Reactive Components with rx
1210
+
1211
+ The `rx()` function creates inline reactive components for fine-grained updates:
1212
+
1213
+ ```tsx
1214
+ import { rx } from "atomirx/react";
1215
+
1216
+ function Dashboard() {
1217
+ return (
1218
+ <div>
1219
+ {/* Only this span re-renders when count$ changes */}
1220
+ <span>Count: {rx(count$)}</span>
1221
+
1222
+ {/* Derived value */}
1223
+ <span>Doubled: {rx(({ get }) => get(count$) * 2)}</span>
1224
+
1225
+ {/* Complex rendering */}
1226
+ {rx(({ get }) => {
1227
+ const user = get(user$);
1228
+ return user ? <UserCard user={user} /> : <LoginPrompt />;
1229
+ })}
1230
+
1231
+ {/* Async with utilities */}
1232
+ {rx(({ all }) => {
1233
+ const [user, posts] = all(user$, posts$);
1234
+ return <Feed user={user} posts={posts} />;
1235
+ })}
1236
+ </div>
1237
+ );
1238
+ }
1239
+ ```
1240
+
1241
+ **Key benefit**: The parent component doesn't re-render when atoms change - only the `rx` components do.
1242
+
1243
+ ### Async Actions with useAction
1244
+
1245
+ Handle async operations with built-in loading/error states:
1246
+
1247
+ ```tsx
1248
+ import { useAction } from "atomirx/react";
1249
+
1250
+ function UserProfile({ userId }: { userId: string }) {
1251
+ const saveUser = useAction(async ({ signal }, data: UserData) => {
1252
+ const response = await fetch(`/api/users/${userId}`, {
1253
+ method: "PUT",
1254
+ body: JSON.stringify(data),
1255
+ signal, // Automatic abort on unmount or re-execution
1256
+ });
1257
+ return response.json();
1258
+ });
1259
+
1260
+ return (
1261
+ <form
1262
+ onSubmit={(e) => {
1263
+ e.preventDefault();
1264
+ saveUser(formData);
1265
+ }}
1266
+ >
1267
+ {saveUser.status === "loading" && <Spinner />}
1268
+ {saveUser.status === "error" && <Error message={saveUser.error} />}
1269
+ {saveUser.status === "success" && <Success data={saveUser.result} />}
1270
+
1271
+ <button disabled={saveUser.status === "loading"}>Save</button>
1272
+ </form>
1273
+ );
1274
+ }
1275
+ ```
1276
+
1277
+ #### Eager Execution
1278
+
1279
+ ```tsx
1280
+ // Execute immediately on mount
1281
+ const fetchUser = useAction(
1282
+ async ({ signal }) => fetchUserApi(userId, { signal }),
1283
+ { lazy: false, deps: [userId] }
1284
+ );
1285
+
1286
+ // Re-execute when atom changes
1287
+ const fetchPosts = useAction(
1288
+ async ({ signal }) => fetchPostsApi(filter$.value, { signal }),
1289
+ { lazy: false, deps: [filter$] }
1290
+ );
1291
+ ```
1292
+
1293
+ #### useAction API
1294
+
1295
+ | Property/Method | Type | Description |
1296
+ | --------------- | --------------------------------------------- | ---------------------------- |
1297
+ | `status` | `'idle' \| 'loading' \| 'success' \| 'error'` | Current state |
1298
+ | `result` | `T \| undefined` | Result value when successful |
1299
+ | `error` | `unknown` | Error when failed |
1300
+ | `abort()` | `() => void` | Cancel current request |
1301
+ | `reset()` | `() => void` | Reset to idle state |
1302
+
1303
+ ### Reference Stability with useStable
1304
+
1305
+ Prevent unnecessary re-renders by stabilizing references:
1306
+
1307
+ ```tsx
1308
+ import { useStable } from "atomirx/react";
1309
+
1310
+ function SearchResults({ query, filters }: Props) {
1311
+ const stable = useStable({
1312
+ // Object - stable if shallow equal
1313
+ searchParams: { query, ...filters },
1314
+
1315
+ // Array - stable if items are reference-equal
1316
+ selectedIds: [1, 2, 3],
1317
+
1318
+ // Function - reference never changes
1319
+ onSelect: (id: number) => {
1320
+ console.log("Selected:", id);
1321
+ },
1322
+ });
1323
+
1324
+ // Safe to use in dependency arrays
1325
+ useEffect(() => {
1326
+ performSearch(stable.searchParams);
1327
+ }, [stable.searchParams]);
1328
+
1329
+ return (
1330
+ <MemoizedList params={stable.searchParams} onSelect={stable.onSelect} />
1331
+ );
1332
+ }
1333
+ ```
1334
+
1335
+ ### Suspense Integration
1336
+
1337
+ atomirx is designed to work seamlessly with React Suspense:
1338
+
1339
+ ```tsx
1340
+ import { Suspense } from "react";
1341
+ import { ErrorBoundary } from "react-error-boundary";
1342
+ import { atom } from "atomirx";
1343
+ import { useValue } from "atomirx/react";
1344
+
1345
+ const user$ = atom(fetchUser());
1346
+
1347
+ function UserProfile() {
1348
+ // Suspends until user$ resolves
1349
+ const user = useValue(user$);
1350
+ return <div>{user.name}</div>;
1351
+ }
1352
+
1353
+ function App() {
1354
+ return (
1355
+ <ErrorBoundary fallback={<div>Something went wrong</div>}>
1356
+ <Suspense fallback={<div>Loading...</div>}>
1357
+ <UserProfile />
1358
+ </Suspense>
1359
+ </ErrorBoundary>
1360
+ );
1361
+ }
1362
+ ```
1363
+
1364
+ #### Nested Suspense Boundaries
1365
+
1366
+ ```tsx
1367
+ function Dashboard() {
1368
+ return (
1369
+ <div>
1370
+ <Suspense fallback={<HeaderSkeleton />}>
1371
+ <Header /> {/* Depends on user$ */}
1372
+ </Suspense>
1373
+
1374
+ <Suspense fallback={<FeedSkeleton />}>
1375
+ <Feed /> {/* Depends on posts$ */}
1376
+ </Suspense>
1377
+
1378
+ <Suspense fallback={<SidebarSkeleton />}>
1379
+ <Sidebar /> {/* Depends on recommendations$ */}
1380
+ </Suspense>
1381
+ </div>
1382
+ );
1383
+ }
1384
+ ```
1385
+
1386
+ ## API Reference
1387
+
1388
+ ### Core API
1389
+
1390
+ #### `atom<T>(initialValue, options?)`
1391
+
1392
+ Creates a mutable reactive atom.
1393
+
1394
+ ```typescript
1395
+ function atom<T>(
1396
+ initialValue: T | Promise<T> | (() => T | Promise<T>),
1397
+ options?: { fallback?: T }
1398
+ ): MutableAtom<T>;
1399
+ ```
1400
+
1401
+ #### `derived<T>(selector)`
1402
+
1403
+ Creates a read-only derived atom.
1404
+
1405
+ ```typescript
1406
+ function derived<T>(selector: (context: SelectContext) => T): Atom<T>;
1407
+
1408
+ // Legacy array API (backward compatible)
1409
+ function derived<T, S>(source: Atom<S>, selector: (get: () => S) => T): Atom<T>;
1410
+
1411
+ function derived<T, S extends readonly Atom<any>[]>(
1412
+ sources: S,
1413
+ selector: (...getters: GetterTuple<S>) => T
1414
+ ): Atom<T>;
1415
+ ```
1416
+
1417
+ #### `effect(fn, options?)`
1418
+
1419
+ Creates a side effect that runs when dependencies change.
1420
+
1421
+ ```typescript
1422
+ interface EffectContext extends SelectContext {
1423
+ onCleanup: (cleanup: VoidFunction) => void;
1424
+ onError: (handler: (error: unknown) => void) => void;
1425
+ }
1426
+
1427
+ function effect(
1428
+ fn: (context: EffectContext) => void,
1429
+ options?: { onError?: (error: Error) => void }
1430
+ ): () => void; // Returns dispose function
1431
+ ```
1432
+
1433
+ #### `batch(fn)`
1434
+
1435
+ Batches multiple updates into a single notification.
1436
+
1437
+ ```typescript
1438
+ function batch<T>(fn: () => T): T;
1439
+ ```
1440
+
1441
+ #### `emitter<T>()`
1442
+
1443
+ Creates a pub/sub event emitter.
1444
+
1445
+ ```typescript
1446
+ function emitter<T>(): {
1447
+ on: (listener: (value: T) => void) => () => void;
1448
+ emit: (value: T) => void;
1449
+ settle: (value: T) => void;
1450
+ };
1451
+ ```
1452
+
1453
+ #### `define<T>(factory, options?)`
1454
+
1455
+ Creates a swappable lazy singleton.
1456
+
1457
+ ```typescript
1458
+ function define<T>(
1459
+ factory: () => T,
1460
+ options?: { eager?: boolean }
1461
+ ): {
1462
+ (): T;
1463
+ override: (factory: () => T) => void;
1464
+ reset: () => void;
1465
+ };
1466
+ ```
1467
+
1468
+ #### `isAtom(value)`
1469
+
1470
+ Type guard for atoms.
1471
+
1472
+ ```typescript
1473
+ function isAtom<T>(value: unknown): value is Atom<T>;
1474
+ ```
1475
+
1476
+ ### SelectContext API
1477
+
1478
+ Available in `derived()`, `effect()`, `useValue()`, and `rx()`:
1479
+
1480
+ | Method | Signature | Description |
1481
+ | --------- | ---------------------------------- | -------------------------------------------- |
1482
+ | `get` | `<T>(atom: Atom<T>) => Awaited<T>` | Read atom value with dependency tracking |
1483
+ | `all` | `(...atoms) => [values...]` | Wait for all atoms (Promise.all semantics) |
1484
+ | `any` | `(...atoms) => value` | First ready value (Promise.any semantics) |
1485
+ | `race` | `(...atoms) => value` | First settled value (Promise.race semantics) |
1486
+ | `settled` | `(...atoms) => SettledResult[]` | All results (Promise.allSettled semantics) |
1487
+
1488
+ **Behavior:**
1489
+
1490
+ - `get()`: Returns value if ready, throws error if error, throws Promise if loading
1491
+ - `all()`: Suspends until all atoms are ready, throws on first error
1492
+ - `any()`: Returns first ready value, throws AggregateError if all error
1493
+ - `race()`: Returns first settled (ready or error)
1494
+ - `settled()`: Returns `{ status: "ready", value }` or `{ status: "error", error }` for each atom
1495
+
1496
+ ### React API
1497
+
1498
+ #### `useValue`
1499
+
1500
+ ```typescript
1501
+ // Shorthand
1502
+ function useValue<T>(atom: Atom<T>): T;
1503
+
1504
+ // Context selector
1505
+ function useValue<T>(
1506
+ selector: (context: SelectContext) => T,
1507
+ equals?: (prev: T, next: T) => boolean
1508
+ ): T;
1509
+ ```
1510
+
1511
+ #### `rx`
1512
+
1513
+ ```typescript
1514
+ // Shorthand
1515
+ function rx<T>(atom: Atom<T>): ReactNode;
1516
+
1517
+ // Context selector
1518
+ function rx<T>(
1519
+ selector: (context: SelectContext) => T,
1520
+ equals?: (prev: T, next: T) => boolean
1521
+ ): ReactNode;
1522
+ ```
1523
+
1524
+ #### `useAction`
1525
+
1526
+ ```typescript
1527
+ function useAction<T, Args extends any[]>(
1528
+ action: (context: { signal: AbortSignal }, ...args: Args) => Promise<T>,
1529
+ options?: {
1530
+ lazy?: boolean;
1531
+ deps?: (Atom<any> | any)[];
1532
+ }
1533
+ ): {
1534
+ (...args: Args): void;
1535
+ status: "idle" | "loading" | "success" | "error";
1536
+ result: T | undefined;
1537
+ error: unknown;
1538
+ abort: () => void;
1539
+ reset: () => void;
1540
+ };
1541
+ ```
1542
+
1543
+ #### `useStable`
1544
+
1545
+ ```typescript
1546
+ function useStable<T extends Record<string, any>>(
1547
+ input: T,
1548
+ equals?: (prev: T[keyof T], next: T[keyof T]) => boolean
1549
+ ): T;
1550
+ ```
1551
+
1552
+ ## TypeScript Integration
1553
+
1554
+ atomirx is written in TypeScript and provides full type inference:
1555
+
1556
+ ```typescript
1557
+ import { atom, derived } from "atomirx";
1558
+
1559
+ // Types are automatically inferred
1560
+ const count$ = atom(0); // MutableAtom<number>
1561
+ const name$ = atom("John"); // MutableAtom<string>
1562
+ const doubled$ = derived(({ get }) => get(count$) * 2); // Atom<number>
1563
+
1564
+ // Explicit typing when needed
1565
+ interface User {
1566
+ id: string;
1567
+ name: string;
1568
+ email: string;
1569
+ }
1570
+
1571
+ const user$ = atom<User | null>(null); // MutableAtom<User | null>
1572
+ const userData$ = atom<User>(fetchUser()); // MutableAtom<User>
1573
+
1574
+ // Type-safe selectors
1575
+ const userName$ = derived(({ get }) => {
1576
+ const user = get(user$);
1577
+ return user?.name ?? "Anonymous"; // Atom<string>
1578
+ });
1579
+
1580
+ // Generic atoms
1581
+ function createListAtom<T>(initial: T[] = []) {
1582
+ const items$ = atom<T[]>(initial);
1583
+
1584
+ return {
1585
+ items$,
1586
+ add: (item: T) => items$.set((list) => [...list, item]),
1587
+ remove: (predicate: (item: T) => boolean) =>
1588
+ items$.set((list) => list.filter((item) => !predicate(item))),
1589
+ };
1590
+ }
1591
+
1592
+ const todoList = createListAtom<Todo>();
1593
+ ```
1594
+
1595
+ ## Comparison with Other Libraries
1596
+
1597
+ | Feature | atomirx | Redux Toolkit | Zustand | Jotai | Recoil |
1598
+ | -------------------- | ----------- | ------------- | ------- | ----------- | -------- |
1599
+ | Bundle size | ~3KB | ~12KB | ~3KB | ~8KB | ~20KB |
1600
+ | Boilerplate | Minimal | Low | Minimal | Minimal | Medium |
1601
+ | TypeScript | First-class | First-class | Good | First-class | Good |
1602
+ | Async support | Built-in | RTK Query | Manual | Built-in | Built-in |
1603
+ | Fine-grained updates | Yes | No | Partial | Yes | Yes |
1604
+ | Suspense support | Native | No | No | Yes | Yes |
1605
+ | DevTools | Planned | Yes | Yes | Yes | Yes |
1606
+ | Learning curve | Low | Medium | Low | Low | Medium |
1607
+
1608
+ ### When to Use atomirx
1609
+
1610
+ **Choose atomirx when you want:**
1611
+
1612
+ - Minimal API with maximum capability
1613
+ - First-class async support without additional packages
1614
+ - Fine-grained reactivity for optimal performance
1615
+ - React Suspense integration out of the box
1616
+ - Strong TypeScript inference
1617
+
1618
+ **Consider alternatives when you need:**
1619
+
1620
+ - Time-travel debugging (Redux Toolkit)
1621
+ - Established ecosystem with many plugins (Redux)
1622
+ - Server-side state management (TanStack Query)
1623
+
1624
+ ## Resources & Learning
1625
+
1626
+ ### Documentation
1627
+
1628
+ - [API Reference](#api-reference) - Complete API documentation
1629
+ - [Usage Guide](#usage-guide) - In-depth usage patterns
1630
+ - [React Integration](#react-integration) - React-specific features
1631
+
1632
+ ### Examples
1633
+
1634
+ - [Counter](./examples/counter) - Basic counter example
1635
+ - [Todo App](./examples/todo) - Full todo application
1636
+ - [Async Data](./examples/async) - Async data fetching patterns
1637
+ - [Testing](./examples/testing) - Testing strategies with `define()`
1638
+
1639
+ ### Community
1640
+
1641
+ - [GitHub Issues](https://github.com/atomirx/atomirx/issues) - Bug reports and feature requests
1642
+ - [Discussions](https://github.com/atomirx/atomirx/discussions) - Questions and community support
1643
+
1644
+ ## License
1645
+
1646
+ MIT License
1647
+
1648
+ Copyright (c) atomirx contributors
1649
+
1650
+ Permission is hereby granted, free of charge, to any person obtaining a copy
1651
+ of this software and associated documentation files (the "Software"), to deal
1652
+ in the Software without restriction, including without limitation the rights
1653
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1654
+ copies of the Software, and to permit persons to whom the Software is
1655
+ furnished to do so, subject to the following conditions:
1656
+
1657
+ The above copyright notice and this permission notice shall be included in all
1658
+ copies or substantial portions of the Software.
1659
+
1660
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1661
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1662
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1663
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1664
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1665
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1666
+ SOFTWARE.