@mongez/atom 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1227 -486
- package/cjs/atom.d.ts +1 -1
- package/cjs/atom.d.ts.map +1 -1
- package/cjs/atom.js +16 -13
- package/cjs/atom.js.map +1 -1
- package/cjs/types.d.ts +1 -1
- package/cjs/types.d.ts.map +1 -1
- package/esm/atom.d.ts +1 -1
- package/esm/atom.d.ts.map +1 -1
- package/esm/atom.js +16 -13
- package/esm/atom.js.map +1 -1
- package/esm/types.d.ts +1 -1
- package/esm/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,21 +1,75 @@
|
|
|
1
1
|
# Atoms
|
|
2
2
|
|
|
3
|
-
A powerful state management
|
|
4
|
-
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
A powerful, framework-agnostic state management library that works seamlessly with any UI framework (React, Vue, Angular, Svelte) or Node.js application.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Why Atoms?](#why-atoms)
|
|
8
|
+
- [Features](#features)
|
|
9
|
+
- [Installation](#installation)
|
|
10
|
+
- [Quick Start](#quick-start)
|
|
11
|
+
- [Architecture & Philosophy](#architecture--philosophy)
|
|
12
|
+
- [Core Concepts](#core-concepts)
|
|
13
|
+
- [Creating Atoms](#creating-atoms)
|
|
14
|
+
- [Getting Atom Values](#getting-atom-values)
|
|
15
|
+
- [Updating Atoms](#updating-atoms)
|
|
16
|
+
- [Watching for Changes](#watching-for-changes)
|
|
17
|
+
- [Resetting Atoms](#resetting-atoms)
|
|
18
|
+
- [Working with Objects](#working-with-objects)
|
|
19
|
+
- [Merging Values](#merging-values)
|
|
20
|
+
- [Changing Single Keys](#changing-single-keys)
|
|
21
|
+
- [Getting Nested Values](#getting-nested-values)
|
|
22
|
+
- [Watching Partial Changes](#watching-partial-changes)
|
|
23
|
+
- [Working with Arrays](#working-with-arrays)
|
|
24
|
+
- [Atom Collections](#atom-collections)
|
|
25
|
+
- [Array Methods](#array-methods)
|
|
26
|
+
- [Advanced Features](#advanced-features)
|
|
27
|
+
- [Atom Actions](#atom-actions)
|
|
28
|
+
- [Value Mutation Before Update](#value-mutation-before-update)
|
|
29
|
+
- [Custom Getters](#custom-getters)
|
|
30
|
+
- [Silent Updates](#silent-updates)
|
|
31
|
+
- [Atom Cloning](#atom-cloning)
|
|
32
|
+
- [Complete API Reference](#complete-api-reference)
|
|
33
|
+
- [Framework Integration](#framework-integration)
|
|
34
|
+
- [React](#react)
|
|
35
|
+
- [Vue 3](#vue-3)
|
|
36
|
+
- [Angular](#angular)
|
|
37
|
+
- [Svelte](#svelte)
|
|
38
|
+
- [Vanilla JavaScript](#vanilla-javascript)
|
|
39
|
+
- [Server-Side Usage](#server-side-usage)
|
|
40
|
+
- [Advanced Patterns](#advanced-patterns)
|
|
41
|
+
- [Performance Guide](#performance-guide)
|
|
42
|
+
- [TypeScript Guide](#typescript-guide)
|
|
43
|
+
- [Troubleshooting](#troubleshooting)
|
|
44
|
+
- [Migration Guides](#migration-guides)
|
|
45
|
+
- [Comparison with Other Libraries](#comparison-with-other-libraries)
|
|
46
|
+
- [Change Log](#change-log)
|
|
47
|
+
|
|
48
|
+
## Why Atoms?
|
|
49
|
+
|
|
50
|
+
The main purpose of this package is to provide a **simple, performant, and framework-agnostic** state management solution that:
|
|
51
|
+
|
|
52
|
+
- Works **anywhere** - UI frameworks, Node.js, or vanilla JavaScript
|
|
53
|
+
- Requires **minimal learning curve** - master it in minutes
|
|
54
|
+
- Provides **fine-grained reactivity** - subscribe to specific property changes
|
|
55
|
+
- Maintains **immutability** - original values are never mutated
|
|
56
|
+
- Offers **extensibility** - custom actions and behaviors
|
|
57
|
+
- Delivers **excellent performance** - event-driven architecture with minimal overhead
|
|
8
58
|
|
|
9
59
|
## Features
|
|
10
60
|
|
|
11
|
-
- Simple and easy to use
|
|
12
|
-
- Won't take more than few minutes to **master it
|
|
13
|
-
- Can be used outside components
|
|
14
|
-
- Listen to atom's value change
|
|
15
|
-
- Listen to atom's object property change
|
|
16
|
-
- Lightweight in size
|
|
17
|
-
- Easy Managing Objects, Arrays, and booleans
|
|
18
|
-
- Open for Extension using Atom Actions
|
|
61
|
+
- ✅ Simple and easy to use
|
|
62
|
+
- ✅ Won't take more than few minutes to **master it**
|
|
63
|
+
- ✅ Can be used outside components
|
|
64
|
+
- ✅ Listen to atom's value change
|
|
65
|
+
- ✅ Listen to atom's object property change
|
|
66
|
+
- ✅ Lightweight in size
|
|
67
|
+
- ✅ Easy Managing Objects, Arrays, and booleans
|
|
68
|
+
- ✅ Open for Extension using Atom Actions
|
|
69
|
+
- ✅ Framework-agnostic (React, Vue, Angular, Svelte, etc.)
|
|
70
|
+
- ✅ TypeScript support with full type inference
|
|
71
|
+
- ✅ Immutable by default
|
|
72
|
+
- ✅ Event-driven architecture
|
|
19
73
|
|
|
20
74
|
## React Atom
|
|
21
75
|
|
|
@@ -23,127 +77,117 @@ For React users, we have a [dedicated package](https://github.com/hassanzohdy/mo
|
|
|
23
77
|
|
|
24
78
|
## Installation
|
|
25
79
|
|
|
26
|
-
|
|
80
|
+
```bash
|
|
81
|
+
yarn add @mongez/atom
|
|
82
|
+
```
|
|
27
83
|
|
|
28
84
|
Or
|
|
29
85
|
|
|
30
|
-
|
|
86
|
+
```bash
|
|
87
|
+
npm i @mongez/atom
|
|
88
|
+
```
|
|
31
89
|
|
|
32
90
|
Or
|
|
33
91
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
Atoms are meant to be **unique** therefore the atom `key` can not be used in more than one atom, if other atom is being created with a previously defined atom, an error will be thrown that indicates to use another atom key.
|
|
39
|
-
|
|
40
|
-
## Create A New Atom
|
|
92
|
+
```bash
|
|
93
|
+
pnpm add @mongez/atom
|
|
94
|
+
```
|
|
41
95
|
|
|
42
|
-
|
|
96
|
+
## Quick Start
|
|
43
97
|
|
|
44
|
-
|
|
98
|
+
Get started in 30 seconds:
|
|
45
99
|
|
|
46
100
|
```ts
|
|
47
|
-
import { createAtom
|
|
101
|
+
import { createAtom } from "@mongez/atom";
|
|
48
102
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
103
|
+
// 1. Create an atom
|
|
104
|
+
const counterAtom = createAtom({
|
|
105
|
+
key: "counter",
|
|
106
|
+
default: 0,
|
|
52
107
|
});
|
|
53
|
-
```
|
|
54
108
|
|
|
55
|
-
|
|
109
|
+
// 2. Read the value
|
|
110
|
+
console.log(counterAtom.value); // 0
|
|
56
111
|
|
|
57
|
-
|
|
112
|
+
// 3. Update the value
|
|
113
|
+
counterAtom.update(1);
|
|
58
114
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
115
|
+
// 4. Listen for changes
|
|
116
|
+
counterAtom.onChange((newValue, oldValue) => {
|
|
117
|
+
console.log(`Counter changed from ${oldValue} to ${newValue}`);
|
|
118
|
+
});
|
|
62
119
|
|
|
63
|
-
|
|
120
|
+
// 5. Update again to see the listener fire
|
|
121
|
+
counterAtom.update(2); // Logs: "Counter changed from 1 to 2"
|
|
122
|
+
```
|
|
64
123
|
|
|
65
|
-
|
|
124
|
+
That's it! You now understand 80% of what atoms do. Continue reading for advanced features.
|
|
66
125
|
|
|
67
|
-
|
|
68
|
-
import { useEffect, useState } from "react";
|
|
69
|
-
import { currencyAtom } from "~/src/atoms";
|
|
126
|
+
## Architecture & Philosophy
|
|
70
127
|
|
|
71
|
-
|
|
72
|
-
const [currency, setCurrency] = useState(currencyAtom.value);
|
|
128
|
+
### Event-Driven Design
|
|
73
129
|
|
|
74
|
-
|
|
75
|
-
// Watch for currency changes
|
|
76
|
-
return currencyAtom.onChange(setCurrency);
|
|
77
|
-
}, []);
|
|
130
|
+
Atoms use an event-driven architecture powered by [@mongez/events](https://github.com/hassanzohdy/mongez-events). When an atom's value changes, it emits an event that all subscribers receive. This allows for:
|
|
78
131
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
Currency: {currency}
|
|
83
|
-
</>
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
```
|
|
132
|
+
- **Decoupled components** - Components don't need to know about each other
|
|
133
|
+
- **Fine-grained updates** - Subscribe only to what you need
|
|
134
|
+
- **Predictable behavior** - Changes flow in one direction
|
|
87
135
|
|
|
88
|
-
|
|
136
|
+
### Immutability
|
|
89
137
|
|
|
90
|
-
|
|
138
|
+
All atoms are **immutable by default**. When you create an atom with an object or array as the default value, that original value is cloned and preserved:
|
|
91
139
|
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
140
|
+
```ts
|
|
141
|
+
const original = { name: "John" };
|
|
142
|
+
const userAtom = createAtom({
|
|
143
|
+
key: "user",
|
|
144
|
+
default: original,
|
|
145
|
+
});
|
|
98
146
|
|
|
99
|
-
|
|
147
|
+
userAtom.update({ name: "Jane" });
|
|
100
148
|
|
|
101
|
-
|
|
149
|
+
console.log(original); // { name: "John" } - unchanged!
|
|
150
|
+
console.log(userAtom.value); // { name: "Jane" }
|
|
151
|
+
```
|
|
102
152
|
|
|
103
|
-
|
|
153
|
+
### Framework Agnostic
|
|
104
154
|
|
|
105
|
-
|
|
155
|
+
Unlike Redux (React-focused) or Pinia (Vue-focused), atoms work **everywhere**:
|
|
106
156
|
|
|
107
|
-
|
|
157
|
+
- **UI Frameworks**: React, Vue, Angular, Svelte, Solid, Qwik
|
|
158
|
+
- **Server-Side**: Node.js, Deno, Bun
|
|
159
|
+
- **Vanilla JS**: No framework needed
|
|
108
160
|
|
|
109
|
-
|
|
161
|
+
This makes atoms perfect for:
|
|
110
162
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
163
|
+
- Shared logic between frontend and backend
|
|
164
|
+
- Migrating between frameworks
|
|
165
|
+
- Building framework-agnostic libraries
|
|
114
166
|
|
|
115
|
-
|
|
116
|
-
```
|
|
167
|
+
### Single Responsibility
|
|
117
168
|
|
|
118
|
-
|
|
169
|
+
Each atom manages **one piece of state**. This promotes:
|
|
119
170
|
|
|
120
|
-
|
|
171
|
+
- **Better organization** - Easy to find and reason about
|
|
172
|
+
- **Easier testing** - Test atoms in isolation
|
|
173
|
+
- **Improved performance** - Only relevant subscribers are notified
|
|
121
174
|
|
|
122
|
-
|
|
123
|
-
// anywhere in your app
|
|
124
|
-
import { currencyAtom } from "~/src/atoms";
|
|
175
|
+
## Core Concepts
|
|
125
176
|
|
|
126
|
-
|
|
127
|
-
```
|
|
177
|
+
### Creating Atoms
|
|
128
178
|
|
|
129
|
-
|
|
179
|
+
Atoms are unique by their `key`. Each atom must have a unique key - attempting to create two atoms with the same key will throw an error.
|
|
130
180
|
|
|
131
181
|
```ts
|
|
132
|
-
|
|
133
|
-
import { currencyAtom } from "~/src/atoms";
|
|
182
|
+
import { createAtom, Atom } from "@mongez/atom";
|
|
134
183
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
184
|
+
// Simple value atom
|
|
185
|
+
export const currencyAtom = createAtom<string>({
|
|
186
|
+
key: "currency",
|
|
187
|
+
default: "EUR",
|
|
138
188
|
});
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
> Please do remember that `atom.update` must receive a new reference of the value, otherwise it will not trigger the change event, for example `atom.update({ ...user })` will trigger the change event.
|
|
142
|
-
|
|
143
|
-
```ts
|
|
144
|
-
// /src/atoms/user-atom.ts
|
|
145
|
-
import { createAtom } from "@mongez/atom";
|
|
146
189
|
|
|
190
|
+
// Object atom
|
|
147
191
|
export type UserData = {
|
|
148
192
|
name: string;
|
|
149
193
|
email: string;
|
|
@@ -160,299 +204,365 @@ export const userAtom = createAtom<UserData>({
|
|
|
160
204
|
id: 1,
|
|
161
205
|
},
|
|
162
206
|
});
|
|
207
|
+
|
|
208
|
+
// Array atom (use atomCollection for better array methods)
|
|
209
|
+
export const todosAtom = createAtom<string[]>({
|
|
210
|
+
key: "todos",
|
|
211
|
+
default: [],
|
|
212
|
+
});
|
|
163
213
|
```
|
|
164
214
|
|
|
165
|
-
|
|
215
|
+
**TypeScript Tip**: Always pass the value type as a generic for full type safety.
|
|
216
|
+
|
|
217
|
+
### Getting Atom Values
|
|
218
|
+
|
|
219
|
+
Access atom values using the `.value` property:
|
|
166
220
|
|
|
167
221
|
```ts
|
|
168
|
-
|
|
222
|
+
import { currencyAtom } from "~/atoms";
|
|
169
223
|
|
|
170
|
-
|
|
224
|
+
console.log(currencyAtom.value); // "EUR"
|
|
171
225
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
226
|
+
// For objects, you get the full object
|
|
227
|
+
console.log(userAtom.value); // { name: "Hasan", age: 30, ... }
|
|
228
|
+
|
|
229
|
+
// For nested values, use .get()
|
|
230
|
+
console.log(userAtom.get("name")); // "Hasan"
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Updating Atoms
|
|
234
|
+
|
|
235
|
+
The basic way to update an atom is with `.update()`:
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
// Direct value
|
|
239
|
+
currencyAtom.update("USD");
|
|
240
|
+
|
|
241
|
+
// Callback with old value
|
|
242
|
+
currencyAtom.update((oldValue, atom) => {
|
|
243
|
+
console.log(`Changing from ${oldValue}`);
|
|
244
|
+
return "GBP";
|
|
175
245
|
});
|
|
176
246
|
```
|
|
177
247
|
|
|
178
|
-
|
|
248
|
+
**Important**: For objects and arrays, you must pass a **new reference** to trigger change events:
|
|
179
249
|
|
|
180
250
|
```ts
|
|
181
|
-
//
|
|
251
|
+
// ❌ Won't trigger change event
|
|
252
|
+
userAtom.update(userAtom.value);
|
|
182
253
|
|
|
183
|
-
|
|
254
|
+
// ✅ Triggers change event
|
|
255
|
+
userAtom.update({ ...userAtom.value });
|
|
256
|
+
```
|
|
184
257
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
258
|
+
### Watching for Changes
|
|
259
|
+
|
|
260
|
+
Subscribe to atom changes with `.onChange()`:
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
const subscription = currencyAtom.onChange((newValue, oldValue, atom) => {
|
|
264
|
+
console.log(`Currency changed from ${oldValue} to ${newValue}`);
|
|
190
265
|
});
|
|
266
|
+
|
|
267
|
+
// Later, unsubscribe
|
|
268
|
+
subscription.unsubscribe();
|
|
191
269
|
```
|
|
192
270
|
|
|
193
|
-
|
|
271
|
+
The `onChange` method returns an [EventSubscription](https://github.com/hassanzohdy/mongez-events#event-subscription) with an `unsubscribe` method.
|
|
272
|
+
|
|
273
|
+
### Resetting Atoms
|
|
194
274
|
|
|
195
|
-
|
|
275
|
+
Reset an atom to its default value:
|
|
196
276
|
|
|
197
277
|
```ts
|
|
198
|
-
|
|
199
|
-
|
|
278
|
+
currencyAtom.update("USD");
|
|
279
|
+
console.log(currencyAtom.value); // "USD"
|
|
200
280
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
age: number;
|
|
205
|
-
id: number;
|
|
206
|
-
};
|
|
281
|
+
currencyAtom.reset();
|
|
282
|
+
console.log(currencyAtom.value); // "EUR" (default value)
|
|
283
|
+
```
|
|
207
284
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
email: "hassanzohdy@gmail.com",
|
|
214
|
-
id: 1,
|
|
215
|
-
},
|
|
285
|
+
Listen for reset events:
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
currencyAtom.onReset((atom) => {
|
|
289
|
+
console.log("Atom was reset!");
|
|
216
290
|
});
|
|
217
291
|
```
|
|
218
292
|
|
|
219
|
-
|
|
293
|
+
## Working with Objects
|
|
220
294
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
295
|
+
When an atom's default value is an object, it gets **special treatment** with additional methods.
|
|
296
|
+
|
|
297
|
+
### Merging Values
|
|
298
|
+
|
|
299
|
+
Instead of spreading the old value manually, use `.merge()`:
|
|
224
300
|
|
|
301
|
+
```ts
|
|
302
|
+
// Without merge
|
|
225
303
|
userAtom.update({
|
|
226
304
|
...userAtom.value,
|
|
227
305
|
name: "Ahmed",
|
|
228
306
|
age: 25,
|
|
229
307
|
});
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
If you notice, we've to spread the old value and then add the new values, this is good, but we can use `atom.merge` instead.
|
|
233
|
-
|
|
234
|
-
```ts
|
|
235
|
-
// anywhere in your app
|
|
236
|
-
import { userAtom } from "~/src/atoms";
|
|
237
308
|
|
|
309
|
+
// With merge (cleaner!)
|
|
238
310
|
userAtom.merge({
|
|
239
311
|
name: "Ahmed",
|
|
240
312
|
age: 25,
|
|
241
313
|
});
|
|
242
314
|
```
|
|
243
315
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
## On Atom Reset
|
|
316
|
+
### Changing Single Keys
|
|
247
317
|
|
|
248
|
-
|
|
318
|
+
Update a single property with `.change()`:
|
|
249
319
|
|
|
250
320
|
```ts
|
|
251
|
-
|
|
252
|
-
|
|
321
|
+
userAtom.change("name", "Ahmed");
|
|
322
|
+
userAtom.change("age", 25);
|
|
323
|
+
```
|
|
253
324
|
|
|
254
|
-
|
|
255
|
-
|
|
325
|
+
This is equivalent to:
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
userAtom.update({
|
|
329
|
+
...userAtom.value,
|
|
330
|
+
name: "Ahmed",
|
|
256
331
|
});
|
|
257
332
|
```
|
|
258
333
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
## Changing only single key in the atom's value
|
|
334
|
+
### Getting Nested Values
|
|
262
335
|
|
|
263
|
-
|
|
336
|
+
Use `.get()` to retrieve values from object atoms:
|
|
264
337
|
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
|
|
338
|
+
```ts
|
|
339
|
+
const userAtom = createAtom({
|
|
340
|
+
key: "user",
|
|
341
|
+
default: {
|
|
342
|
+
name: "Hasan",
|
|
343
|
+
address: {
|
|
344
|
+
city: "New York",
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
});
|
|
268
348
|
|
|
269
|
-
|
|
270
|
-
|
|
349
|
+
// Simple key
|
|
350
|
+
console.log(userAtom.get("name")); // "Hasan"
|
|
271
351
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}, []);
|
|
352
|
+
// Dot notation for nested values
|
|
353
|
+
console.log(userAtom.get("address.city")); // "New York"
|
|
275
354
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
<h1>User Form</h1>
|
|
279
|
-
<input
|
|
280
|
-
type="text"
|
|
281
|
-
value={user.name}
|
|
282
|
-
onChange={(e) => userAtom.change("name", e.target.value)}
|
|
283
|
-
/>
|
|
284
|
-
<input
|
|
285
|
-
type="text"
|
|
286
|
-
value={user.email}
|
|
287
|
-
onChange={(e) => userAtom.change("email", e.target.value)}
|
|
288
|
-
/>
|
|
289
|
-
</>
|
|
290
|
-
);
|
|
291
|
-
}
|
|
355
|
+
// Default value if key doesn't exist
|
|
356
|
+
console.log(userAtom.get("email", "default@email.com")); // "default@email.com"
|
|
292
357
|
```
|
|
293
358
|
|
|
294
|
-
|
|
359
|
+
### Watching Partial Changes
|
|
295
360
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
## Get Atom single key value
|
|
299
|
-
|
|
300
|
-
If atom's value is an object, we can get a value from the atom directly using `atom.get` function.
|
|
361
|
+
Watch for changes to specific properties with `.watch()`:
|
|
301
362
|
|
|
302
363
|
```ts
|
|
303
|
-
import { createAtom } from "@mongez/atom-react";
|
|
304
|
-
|
|
305
364
|
const userAtom = createAtom({
|
|
306
365
|
key: "user",
|
|
307
366
|
default: {
|
|
308
|
-
|
|
367
|
+
name: "Hasan",
|
|
309
368
|
address: {
|
|
310
369
|
city: "New York",
|
|
311
370
|
},
|
|
312
371
|
},
|
|
313
372
|
});
|
|
314
373
|
|
|
315
|
-
|
|
316
|
-
|
|
374
|
+
// Watch a single property
|
|
375
|
+
userAtom.watch("name", (newName, oldName) => {
|
|
376
|
+
console.log(`Name changed from ${oldName} to ${newName}`);
|
|
377
|
+
});
|
|
317
378
|
|
|
318
|
-
|
|
379
|
+
// Watch nested property (dot notation)
|
|
380
|
+
userAtom.watch("address.city", (newCity, oldCity) => {
|
|
381
|
+
console.log(`City changed from ${oldCity} to ${newCity}`);
|
|
382
|
+
});
|
|
319
383
|
|
|
320
|
-
|
|
321
|
-
|
|
384
|
+
// Update triggers only relevant watchers
|
|
385
|
+
userAtom.update({
|
|
386
|
+
...userAtom.value,
|
|
387
|
+
name: "Ali", // Only name watcher fires
|
|
388
|
+
});
|
|
322
389
|
```
|
|
323
390
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
```ts
|
|
327
|
-
console.log(userAtom.get("email", "default@email.com")); // default@email.com
|
|
328
|
-
```
|
|
391
|
+
## Working with Arrays
|
|
329
392
|
|
|
330
|
-
|
|
393
|
+
For arrays, use `atomCollection` instead of `createAtom` to get array-specific methods.
|
|
331
394
|
|
|
332
|
-
|
|
395
|
+
### Atom Collections
|
|
333
396
|
|
|
334
397
|
```ts
|
|
335
|
-
|
|
336
|
-
import { currencyAtom } from "~/src/atoms";
|
|
398
|
+
import { atomCollection } from "@mongez/atom";
|
|
337
399
|
|
|
338
|
-
|
|
400
|
+
const todoListAtom = atomCollection({
|
|
401
|
+
key: "todos",
|
|
402
|
+
default: [],
|
|
403
|
+
});
|
|
339
404
|
```
|
|
340
405
|
|
|
341
|
-
|
|
406
|
+
### Array Methods
|
|
342
407
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
Works exactly like `update` method, but it will not trigger the change event.
|
|
408
|
+
#### Add Items
|
|
346
409
|
|
|
347
410
|
```ts
|
|
348
|
-
//
|
|
349
|
-
|
|
411
|
+
// Add to end
|
|
412
|
+
todoListAtom.push("Buy Milk");
|
|
413
|
+
todoListAtom.push("Buy Bread", "Buy Eggs"); // Multiple items
|
|
350
414
|
|
|
351
|
-
|
|
415
|
+
// Add to beginning
|
|
416
|
+
todoListAtom.unshift("Wake Up");
|
|
417
|
+
todoListAtom.unshift("Shower", "Breakfast"); // Multiple items
|
|
352
418
|
```
|
|
353
419
|
|
|
354
|
-
|
|
420
|
+
#### Remove Items
|
|
355
421
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
// Header.tsx
|
|
360
|
-
import { currencyAtom } from "~/src/atoms";
|
|
361
|
-
import { useEffect } from "react";
|
|
422
|
+
```ts
|
|
423
|
+
// Remove from end
|
|
424
|
+
todoListAtom.pop();
|
|
362
425
|
|
|
363
|
-
|
|
364
|
-
|
|
426
|
+
// Remove from beginning
|
|
427
|
+
todoListAtom.shift();
|
|
365
428
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
}, []);
|
|
429
|
+
// Remove by index
|
|
430
|
+
todoListAtom.remove(0);
|
|
369
431
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
<h1>Header</h1>
|
|
373
|
-
Currency: {currency}
|
|
374
|
-
</>
|
|
375
|
-
);
|
|
376
|
-
}
|
|
377
|
-
```
|
|
432
|
+
// Remove by callback
|
|
433
|
+
todoListAtom.remove((item) => item === "Buy Milk");
|
|
378
434
|
|
|
379
|
-
|
|
435
|
+
// Remove specific item (first occurrence)
|
|
436
|
+
todoListAtom.removeItem("Buy Milk");
|
|
380
437
|
|
|
381
|
-
|
|
438
|
+
// Remove all occurrences
|
|
439
|
+
todoListAtom.removeAll("Buy Milk");
|
|
440
|
+
```
|
|
382
441
|
|
|
383
|
-
|
|
442
|
+
#### Get Items
|
|
384
443
|
|
|
385
444
|
```ts
|
|
386
|
-
//
|
|
387
|
-
|
|
445
|
+
// By index
|
|
446
|
+
console.log(todoListAtom.get(0)); // First item
|
|
388
447
|
|
|
389
|
-
|
|
390
|
-
|
|
448
|
+
// By callback
|
|
449
|
+
console.log(todoListAtom.get((item) => item.includes("Milk"))); // "Buy Milk"
|
|
391
450
|
|
|
392
|
-
|
|
451
|
+
// Find index
|
|
452
|
+
const index = todoListAtom.index((item) => item === "Buy Milk");
|
|
453
|
+
```
|
|
393
454
|
|
|
394
|
-
|
|
455
|
+
#### Update Items
|
|
395
456
|
|
|
396
457
|
```ts
|
|
397
|
-
//
|
|
398
|
-
|
|
458
|
+
// Replace item at index
|
|
459
|
+
todoListAtom.replace(0, "Buy Bread");
|
|
399
460
|
|
|
400
|
-
|
|
401
|
-
|
|
461
|
+
// Map over items (updates the atom)
|
|
462
|
+
todoListAtom.map((item) => item.toUpperCase());
|
|
402
463
|
|
|
403
|
-
|
|
464
|
+
// Iterate (doesn't update)
|
|
465
|
+
todoListAtom.forEach((item, index) => {
|
|
466
|
+
console.log(`${index}: ${item}`);
|
|
467
|
+
});
|
|
468
|
+
```
|
|
404
469
|
|
|
405
|
-
|
|
470
|
+
#### Get Length
|
|
406
471
|
|
|
407
472
|
```ts
|
|
408
|
-
//
|
|
409
|
-
import { currencyAtom } from "~/src/atoms";
|
|
410
|
-
|
|
411
|
-
console.log(currencyAtom.key); // currencyAtom
|
|
473
|
+
console.log(todoListAtom.length); // Number of items
|
|
412
474
|
```
|
|
413
475
|
|
|
414
|
-
##
|
|
476
|
+
## Advanced Features
|
|
415
477
|
|
|
416
|
-
|
|
478
|
+
### Atom Actions
|
|
479
|
+
|
|
480
|
+
Extend atoms with custom methods using `actions`:
|
|
417
481
|
|
|
418
482
|
```ts
|
|
419
|
-
|
|
420
|
-
|
|
483
|
+
export const userAtom = createAtom({
|
|
484
|
+
key: "user",
|
|
485
|
+
default: {
|
|
486
|
+
name: "Hasan",
|
|
487
|
+
age: 30,
|
|
488
|
+
email: "",
|
|
489
|
+
},
|
|
490
|
+
actions: {
|
|
491
|
+
changeName(name: string) {
|
|
492
|
+
this.update({
|
|
493
|
+
...this.value,
|
|
494
|
+
name,
|
|
495
|
+
});
|
|
496
|
+
},
|
|
497
|
+
changeEmail(email: string) {
|
|
498
|
+
this.update({
|
|
499
|
+
...this.value,
|
|
500
|
+
email,
|
|
501
|
+
});
|
|
502
|
+
},
|
|
503
|
+
incrementAge() {
|
|
504
|
+
this.change("age", this.value.age + 1);
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
});
|
|
421
508
|
|
|
422
|
-
|
|
509
|
+
// Usage
|
|
510
|
+
userAtom.changeName("Ahmed");
|
|
511
|
+
userAtom.changeEmail("ahmed@example.com");
|
|
512
|
+
userAtom.incrementAge();
|
|
423
513
|
```
|
|
424
514
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
Sometimes we may need to handle the `atom.get` function to get the data in a customized way, we can achieve this by defining in the atom function call how the atom will retrieve the object's value.
|
|
428
|
-
|
|
429
|
-
Without Defining the `atom getter`
|
|
515
|
+
**Type-Safe Actions**:
|
|
430
516
|
|
|
431
517
|
```ts
|
|
432
|
-
|
|
518
|
+
type UserActions = {
|
|
519
|
+
changeName(name: string): void;
|
|
520
|
+
changeEmail(email: string): void;
|
|
521
|
+
incrementAge(): void;
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
export const userAtom = createAtom<UserData, UserActions>({
|
|
433
525
|
key: "user",
|
|
434
526
|
default: {
|
|
435
|
-
|
|
436
|
-
|
|
527
|
+
/* ... */
|
|
528
|
+
},
|
|
529
|
+
actions: {
|
|
530
|
+
/* ... */
|
|
437
531
|
},
|
|
438
532
|
});
|
|
533
|
+
```
|
|
439
534
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
535
|
+
All action methods are automatically bound to the atom instance, so `this` always refers to the atom.
|
|
536
|
+
|
|
537
|
+
### Value Mutation Before Update
|
|
538
|
+
|
|
539
|
+
Transform values before they're stored using `beforeUpdate`:
|
|
540
|
+
|
|
541
|
+
```ts
|
|
542
|
+
const multipleAtom = createAtom({
|
|
543
|
+
key: "multiple",
|
|
544
|
+
default: 0,
|
|
545
|
+
beforeUpdate(newNumber: number): number {
|
|
546
|
+
return newNumber * 2;
|
|
445
547
|
},
|
|
446
548
|
});
|
|
447
549
|
|
|
448
|
-
|
|
550
|
+
multipleAtom.update(4);
|
|
551
|
+
console.log(multipleAtom.value); // 8
|
|
449
552
|
```
|
|
450
553
|
|
|
451
|
-
|
|
554
|
+
This is useful for:
|
|
452
555
|
|
|
453
|
-
|
|
454
|
-
|
|
556
|
+
- Validation
|
|
557
|
+
- Normalization
|
|
558
|
+
- Data transformation
|
|
559
|
+
- Side effects
|
|
560
|
+
|
|
561
|
+
### Custom Getters
|
|
455
562
|
|
|
563
|
+
Customize how values are retrieved with a custom `get` function:
|
|
564
|
+
|
|
565
|
+
```ts
|
|
456
566
|
const settingsAtom = createAtom({
|
|
457
567
|
key: "settings",
|
|
458
568
|
default: {
|
|
@@ -460,403 +570,1034 @@ const settingsAtom = createAtom({
|
|
|
460
570
|
settings: {},
|
|
461
571
|
},
|
|
462
572
|
get(key: string, defaultValue: any = null, atomValue: any) {
|
|
573
|
+
// Check top-level first, then nested settings
|
|
463
574
|
return atomValue[key] !== undefined
|
|
464
575
|
? atomValue[key]
|
|
465
576
|
: atomValue.settings[key] !== undefined
|
|
466
|
-
|
|
467
|
-
|
|
577
|
+
? atomValue.settings[key]
|
|
578
|
+
: defaultValue;
|
|
468
579
|
},
|
|
469
580
|
});
|
|
470
581
|
|
|
471
|
-
// later
|
|
472
582
|
settingsAtom.update({
|
|
473
583
|
isLoaded: true,
|
|
474
584
|
settings: {
|
|
475
|
-
websiteName: "My Website
|
|
585
|
+
websiteName: "My Website",
|
|
476
586
|
},
|
|
477
587
|
});
|
|
478
588
|
|
|
479
|
-
|
|
589
|
+
// Custom getter allows direct access
|
|
590
|
+
console.log(settingsAtom.get("websiteName")); // "My Website"
|
|
591
|
+
console.log(settingsAtom.get("isLoaded")); // true
|
|
480
592
|
```
|
|
481
593
|
|
|
482
|
-
|
|
594
|
+
### Silent Updates
|
|
483
595
|
|
|
484
|
-
|
|
596
|
+
Update without triggering change events:
|
|
485
597
|
|
|
486
598
|
```ts
|
|
487
|
-
//
|
|
488
|
-
|
|
599
|
+
// Silent update
|
|
600
|
+
currencyAtom.silentUpdate("USD"); // No onChange listeners fire
|
|
489
601
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
602
|
+
// Silent change (for objects)
|
|
603
|
+
userAtom.silentChange("name", "Ahmed"); // No onChange listeners fire
|
|
604
|
+
|
|
605
|
+
// Silent reset
|
|
606
|
+
currencyAtom.silentReset(); // Resets to default, fires onReset but not onChange
|
|
493
607
|
```
|
|
494
608
|
|
|
495
|
-
|
|
609
|
+
**Use Cases**:
|
|
610
|
+
|
|
611
|
+
- Initializing state without triggering effects
|
|
612
|
+
- Cleanup on component unmount
|
|
613
|
+
- Batch updates (update silently, then trigger once)
|
|
614
|
+
|
|
615
|
+
### Atom Cloning
|
|
616
|
+
|
|
617
|
+
Create a copy of an atom with a new key:
|
|
496
618
|
|
|
497
619
|
```ts
|
|
498
|
-
|
|
499
|
-
import { currencyAtom } from "~/src/atoms";
|
|
620
|
+
const clonedAtom = currencyAtom.clone();
|
|
500
621
|
|
|
501
|
-
//
|
|
502
|
-
|
|
503
|
-
useEffect(() => {
|
|
504
|
-
const onCurrencyChange = currencyAtom.onChange(setCurrency);
|
|
505
|
-
return () => onCurrencyChange.unsubscribe();
|
|
506
|
-
}, []);
|
|
622
|
+
console.log(clonedAtom.key); // "currencyCloned1234" (random suffix)
|
|
623
|
+
console.log(clonedAtom.value); // Same as original
|
|
507
624
|
```
|
|
508
625
|
|
|
509
|
-
|
|
626
|
+
Clones are independent - updating one doesn't affect the other.
|
|
510
627
|
|
|
511
|
-
|
|
628
|
+
## Complete API Reference
|
|
512
629
|
|
|
513
|
-
|
|
630
|
+
### Creation Functions
|
|
514
631
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
632
|
+
| Function | Parameters | Returns | Description |
|
|
633
|
+
| ------------------------------ | ------------------- | -------------------------------------- | ----------------------------------------- |
|
|
634
|
+
| `createAtom<Value, Actions>()` | `AtomOptions` | `Atom<Value, Actions>` | Creates a new atom |
|
|
635
|
+
| `atomCollection<Value>()` | `CollectionOptions` | `Atom<Value[], AtomCollectionActions>` | Creates an array atom with helper methods |
|
|
636
|
+
| `getAtom<T>(key)` | `key: string` | `Atom<T> \| undefined` | Gets an existing atom by key |
|
|
637
|
+
| `atomsList()` | - | `Atom<any>[]` | Returns array of all atoms |
|
|
638
|
+
| `atomsObject()` | - | `Record<string, Atom>` | Returns object of all atoms |
|
|
518
639
|
|
|
519
|
-
|
|
520
|
-
key: "user",
|
|
521
|
-
default: {
|
|
522
|
-
key: "Hasan",
|
|
523
|
-
address: {
|
|
524
|
-
city: "New York",
|
|
525
|
-
},
|
|
526
|
-
},
|
|
527
|
-
});
|
|
640
|
+
### Atom Instance Properties
|
|
528
641
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
642
|
+
| Property | Type | Description |
|
|
643
|
+
| -------------- | -------- | ----------------------------------------------------------- |
|
|
644
|
+
| `key` | `string` | Unique atom identifier |
|
|
645
|
+
| `value` | `Value` | Current atom value (readonly) |
|
|
646
|
+
| `defaultValue` | `Value` | Original default value (readonly) |
|
|
647
|
+
| `type` | `string` | Type of value ("string", "number", "object", "array", etc.) |
|
|
648
|
+
| `length` | `number` | Length of array/string values |
|
|
532
649
|
|
|
533
|
-
|
|
534
|
-
userAtom.update({
|
|
535
|
-
...userAtom.value,
|
|
536
|
-
key: "Ali",
|
|
537
|
-
});
|
|
538
|
-
```
|
|
650
|
+
### Atom Instance Methods
|
|
539
651
|
|
|
540
|
-
|
|
652
|
+
#### Update Methods
|
|
541
653
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
654
|
+
| Method | Parameters | Returns | Description |
|
|
655
|
+
| ---------------- | ------------------------------------ | ------- | -------------------------------------------- |
|
|
656
|
+
| `update()` | `value \| (oldValue, atom) => value` | `void` | Updates atom value |
|
|
657
|
+
| `silentUpdate()` | `value \| (oldValue, atom) => value` | `void` | Updates without triggering onChange |
|
|
658
|
+
| `merge()` | `Partial<Value>` | `void` | Merges object (objects only) |
|
|
659
|
+
| `change()` | `key, value` | `void` | Updates single property (objects only) |
|
|
660
|
+
| `silentChange()` | `key, value` | `void` | Updates property without triggering onChange |
|
|
661
|
+
| `reset()` | - | `void` | Resets to default value |
|
|
662
|
+
| `silentReset()` | - | `void` | Resets without triggering onChange |
|
|
545
663
|
|
|
546
|
-
|
|
547
|
-
key: "user",
|
|
548
|
-
default: {
|
|
549
|
-
key: "Hasan",
|
|
550
|
-
address: {
|
|
551
|
-
city: "New York",
|
|
552
|
-
},
|
|
553
|
-
},
|
|
554
|
-
});
|
|
664
|
+
#### Query Methods
|
|
555
665
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
666
|
+
| Method | Parameters | Returns | Description |
|
|
667
|
+
| ------- | -------------------- | ------- | -------------------------------- |
|
|
668
|
+
| `get()` | `key, defaultValue?` | `any` | Gets value by key (objects only) |
|
|
559
669
|
|
|
560
|
-
|
|
561
|
-
userAtom.update({
|
|
562
|
-
...userAtom.value,
|
|
563
|
-
address: {
|
|
564
|
-
...userAtom.value.address,
|
|
565
|
-
city: "Cairo",
|
|
566
|
-
},
|
|
567
|
-
});
|
|
568
|
-
```
|
|
670
|
+
#### Event Methods
|
|
569
671
|
|
|
570
|
-
|
|
672
|
+
| Method | Parameters | Returns | Description |
|
|
673
|
+
| ------------- | ------------------------------------ | ------------------- | ---------------------------- |
|
|
674
|
+
| `onChange()` | `(newValue, oldValue, atom) => void` | `EventSubscription` | Listens for value changes |
|
|
675
|
+
| `watch()` | `key, (newValue, oldValue) => void` | `EventSubscription` | Listens for property changes |
|
|
676
|
+
| `onReset()` | `(atom) => void` | `EventSubscription` | Listens for reset events |
|
|
677
|
+
| `onDestroy()` | `(atom) => void` | `EventSubscription` | Listens for destruction |
|
|
571
678
|
|
|
572
|
-
|
|
679
|
+
#### Utility Methods
|
|
573
680
|
|
|
574
|
-
|
|
681
|
+
| Method | Parameters | Returns | Description |
|
|
682
|
+
| ----------- | ---------- | ------- | ---------------------------- |
|
|
683
|
+
| `clone()` | - | `Atom` | Creates a copy with new key |
|
|
684
|
+
| `destroy()` | - | `void` | Destroys atom and removes it |
|
|
575
685
|
|
|
576
|
-
|
|
686
|
+
### Collection-Specific Methods
|
|
577
687
|
|
|
578
|
-
|
|
579
|
-
|
|
688
|
+
| Method | Parameters | Returns | Description |
|
|
689
|
+
| -------------- | ------------------- | -------------------- | ------------------------ |
|
|
690
|
+
| `push()` | `...items` | `void` | Adds items to end |
|
|
691
|
+
| `unshift()` | `...items` | `void` | Adds items to beginning |
|
|
692
|
+
| `pop()` | - | `void` | Removes last item |
|
|
693
|
+
| `shift()` | - | `void` | Removes first item |
|
|
694
|
+
| `remove()` | `index \| callback` | `void` | Removes item |
|
|
695
|
+
| `removeItem()` | `item` | `void` | Removes first occurrence |
|
|
696
|
+
| `removeAll()` | `item` | `Value[]` | Removes all occurrences |
|
|
697
|
+
| `get()` | `index \| callback` | `Value \| undefined` | Gets item |
|
|
698
|
+
| `index()` | `callback` | `number` | Finds index |
|
|
699
|
+
| `map()` | `callback` | `Value[]` | Maps and updates |
|
|
700
|
+
| `forEach()` | `callback` | `void` | Iterates items |
|
|
701
|
+
| `replace()` | `index, item` | `void` | Replaces item at index |
|
|
580
702
|
|
|
581
|
-
|
|
582
|
-
key: "multiple",
|
|
583
|
-
default: 0,
|
|
584
|
-
beforeUpdate(newNumber: number): number {
|
|
585
|
-
return newNumber * 2;
|
|
586
|
-
},
|
|
587
|
-
});
|
|
703
|
+
## Framework Integration
|
|
588
704
|
|
|
589
|
-
|
|
705
|
+
### React
|
|
590
706
|
|
|
591
|
-
|
|
707
|
+
For React, use the dedicated [@mongez/react-atom](https://github.com/hassanzohdy/mongez-react-atom) package which provides hooks:
|
|
708
|
+
|
|
709
|
+
```tsx
|
|
710
|
+
import { atom } from "@mongez/react-atom";
|
|
711
|
+
|
|
712
|
+
const counterAtom = atom({
|
|
713
|
+
key: "counter",
|
|
714
|
+
default: 0,
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
function Counter() {
|
|
718
|
+
const count = counterAtom.useValue();
|
|
719
|
+
|
|
720
|
+
return (
|
|
721
|
+
<button onClick={() => counterAtom.update(count + 1)}>
|
|
722
|
+
Count: {count}
|
|
723
|
+
</button>
|
|
724
|
+
);
|
|
725
|
+
}
|
|
592
726
|
```
|
|
593
727
|
|
|
594
|
-
|
|
728
|
+
### Vue 3
|
|
595
729
|
|
|
596
|
-
|
|
730
|
+
Use atoms with Vue's Composition API:
|
|
597
731
|
|
|
598
|
-
```
|
|
599
|
-
|
|
600
|
-
import {
|
|
732
|
+
```vue
|
|
733
|
+
<script setup>
|
|
734
|
+
import { ref, onMounted, onUnmounted } from "vue";
|
|
735
|
+
import { createAtom } from "@mongez/atom";
|
|
601
736
|
|
|
602
|
-
const
|
|
603
|
-
|
|
737
|
+
const counterAtom = createAtom({
|
|
738
|
+
key: "counter",
|
|
739
|
+
default: 0,
|
|
604
740
|
});
|
|
605
|
-
```
|
|
606
741
|
|
|
607
|
-
|
|
742
|
+
const count = ref(counterAtom.value);
|
|
608
743
|
|
|
609
|
-
|
|
744
|
+
let subscription;
|
|
610
745
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
746
|
+
onMounted(() => {
|
|
747
|
+
subscription = counterAtom.onChange((newValue) => {
|
|
748
|
+
count.value = newValue;
|
|
749
|
+
});
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
onUnmounted(() => {
|
|
753
|
+
subscription.unsubscribe();
|
|
615
754
|
});
|
|
616
755
|
|
|
617
|
-
|
|
756
|
+
const increment = () => {
|
|
757
|
+
counterAtom.update(counterAtom.value + 1);
|
|
758
|
+
};
|
|
759
|
+
</script>
|
|
760
|
+
|
|
761
|
+
<template>
|
|
762
|
+
<button @click="increment">Count: {{ count }}</button>
|
|
763
|
+
</template>
|
|
618
764
|
```
|
|
619
765
|
|
|
620
|
-
|
|
766
|
+
**Composable Pattern**:
|
|
621
767
|
|
|
622
|
-
```
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
768
|
+
```ts
|
|
769
|
+
// useAtom.ts
|
|
770
|
+
import { ref, onMounted, onUnmounted } from "vue";
|
|
771
|
+
import type { Atom } from "@mongez/atom";
|
|
772
|
+
|
|
773
|
+
export function useAtom<T>(atom: Atom<T>) {
|
|
774
|
+
const value = ref(atom.value);
|
|
775
|
+
let subscription;
|
|
776
|
+
|
|
777
|
+
onMounted(() => {
|
|
778
|
+
subscription = atom.onChange((newValue) => {
|
|
779
|
+
value.value = newValue;
|
|
780
|
+
});
|
|
781
|
+
});
|
|
627
782
|
|
|
628
|
-
|
|
783
|
+
onUnmounted(() => {
|
|
784
|
+
subscription?.unsubscribe();
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
return value;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Usage
|
|
791
|
+
import { useAtom } from "./useAtom";
|
|
792
|
+
const count = useAtom(counterAtom);
|
|
629
793
|
```
|
|
630
794
|
|
|
631
|
-
|
|
795
|
+
### Angular
|
|
632
796
|
|
|
633
|
-
|
|
797
|
+
Integrate atoms with Angular services:
|
|
634
798
|
|
|
635
799
|
```ts
|
|
636
|
-
|
|
800
|
+
// counter.service.ts
|
|
801
|
+
import { Injectable, OnDestroy } from "@angular/core";
|
|
802
|
+
import { BehaviorSubject, Observable } from "rxjs";
|
|
803
|
+
import { createAtom } from "@mongez/atom";
|
|
804
|
+
import type { EventSubscription } from "@mongez/events";
|
|
805
|
+
|
|
806
|
+
@Injectable({
|
|
807
|
+
providedIn: "root",
|
|
808
|
+
})
|
|
809
|
+
export class CounterService implements OnDestroy {
|
|
810
|
+
private counterAtom = createAtom({
|
|
811
|
+
key: "counter",
|
|
812
|
+
default: 0,
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
private counterSubject = new BehaviorSubject(this.counterAtom.value);
|
|
816
|
+
public counter$: Observable<number> = this.counterSubject.asObservable();
|
|
817
|
+
|
|
818
|
+
private subscription: EventSubscription;
|
|
819
|
+
|
|
820
|
+
constructor() {
|
|
821
|
+
this.subscription = this.counterAtom.onChange((newValue) => {
|
|
822
|
+
this.counterSubject.next(newValue);
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
increment() {
|
|
827
|
+
this.counterAtom.update(this.counterAtom.value + 1);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
ngOnDestroy() {
|
|
831
|
+
this.subscription.unsubscribe();
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
```
|
|
637
835
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
836
|
+
```ts
|
|
837
|
+
// counter.component.ts
|
|
838
|
+
import { Component } from "@angular/core";
|
|
839
|
+
import { CounterService } from "./counter.service";
|
|
840
|
+
|
|
841
|
+
@Component({
|
|
842
|
+
selector: "app-counter",
|
|
843
|
+
template: `
|
|
844
|
+
<button (click)="increment()">Count: {{ counter$ | async }}</button>
|
|
845
|
+
`,
|
|
846
|
+
})
|
|
847
|
+
export class CounterComponent {
|
|
848
|
+
counter$ = this.counterService.counter$;
|
|
849
|
+
|
|
850
|
+
constructor(private counterService: CounterService) {}
|
|
851
|
+
|
|
852
|
+
increment() {
|
|
853
|
+
this.counterService.increment();
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
### Svelte
|
|
859
|
+
|
|
860
|
+
Use atoms with Svelte stores:
|
|
861
|
+
|
|
862
|
+
```ts
|
|
863
|
+
// atomStore.ts
|
|
864
|
+
import { writable } from "svelte/store";
|
|
865
|
+
import type { Atom } from "@mongez/atom";
|
|
866
|
+
|
|
867
|
+
export function atomStore<T>(atom: Atom<T>) {
|
|
868
|
+
const { subscribe, set } = writable(atom.value);
|
|
869
|
+
|
|
870
|
+
atom.onChange((newValue) => {
|
|
871
|
+
set(newValue);
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
return {
|
|
875
|
+
subscribe,
|
|
876
|
+
update: (value: T) => atom.update(value),
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
```svelte
|
|
882
|
+
<script>
|
|
883
|
+
import { createAtom } from '@mongez/atom';
|
|
884
|
+
import { atomStore } from './atomStore';
|
|
885
|
+
|
|
886
|
+
const counterAtom = createAtom({
|
|
887
|
+
key: 'counter',
|
|
888
|
+
default: 0,
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
const counter = atomStore(counterAtom);
|
|
892
|
+
|
|
893
|
+
function increment() {
|
|
894
|
+
counter.update($counter + 1);
|
|
895
|
+
}
|
|
896
|
+
</script>
|
|
897
|
+
|
|
898
|
+
<button on:click={increment}>
|
|
899
|
+
Count: {$counter}
|
|
900
|
+
</button>
|
|
901
|
+
```
|
|
902
|
+
|
|
903
|
+
### Vanilla JavaScript
|
|
904
|
+
|
|
905
|
+
Use atoms without any framework:
|
|
906
|
+
|
|
907
|
+
```html
|
|
908
|
+
<!DOCTYPE html>
|
|
909
|
+
<html>
|
|
910
|
+
<body>
|
|
911
|
+
<button id="increment">Count: <span id="count">0</span></button>
|
|
912
|
+
|
|
913
|
+
<script type="module">
|
|
914
|
+
import { createAtom } from "@mongez/atom";
|
|
915
|
+
|
|
916
|
+
const counterAtom = createAtom({
|
|
917
|
+
key: "counter",
|
|
918
|
+
default: 0,
|
|
650
919
|
});
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
920
|
+
|
|
921
|
+
const countElement = document.getElementById("count");
|
|
922
|
+
const button = document.getElementById("increment");
|
|
923
|
+
|
|
924
|
+
// Update UI when atom changes
|
|
925
|
+
counterAtom.onChange((newValue) => {
|
|
926
|
+
countElement.textContent = newValue;
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
// Increment on click
|
|
930
|
+
button.addEventListener("click", () => {
|
|
931
|
+
counterAtom.update(counterAtom.value + 1);
|
|
656
932
|
});
|
|
933
|
+
</script>
|
|
934
|
+
</body>
|
|
935
|
+
</html>
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
## Server-Side Usage
|
|
939
|
+
|
|
940
|
+
Atoms work perfectly in Node.js for managing application state:
|
|
941
|
+
|
|
942
|
+
### Request-Scoped State
|
|
943
|
+
|
|
944
|
+
```ts
|
|
945
|
+
// userContext.ts
|
|
946
|
+
import { createAtom } from "@mongez/atom";
|
|
947
|
+
|
|
948
|
+
export function createUserContext(userId: string) {
|
|
949
|
+
return createAtom({
|
|
950
|
+
key: `user-${userId}`,
|
|
951
|
+
default: {
|
|
952
|
+
id: userId,
|
|
953
|
+
permissions: [],
|
|
954
|
+
preferences: {},
|
|
657
955
|
},
|
|
658
|
-
}
|
|
659
|
-
}
|
|
956
|
+
});
|
|
957
|
+
}
|
|
660
958
|
|
|
661
|
-
//
|
|
959
|
+
// middleware.ts
|
|
960
|
+
app.use((req, res, next) => {
|
|
961
|
+
req.userAtom = createUserContext(req.userId);
|
|
962
|
+
next();
|
|
963
|
+
});
|
|
662
964
|
|
|
663
|
-
|
|
664
|
-
|
|
965
|
+
// route.ts
|
|
966
|
+
app.get("/profile", (req, res) => {
|
|
967
|
+
const user = req.userAtom.value;
|
|
968
|
+
res.json(user);
|
|
969
|
+
});
|
|
665
970
|
```
|
|
666
971
|
|
|
667
|
-
|
|
972
|
+
### Caching Layer
|
|
973
|
+
|
|
974
|
+
```ts
|
|
975
|
+
import { createAtom } from "@mongez/atom";
|
|
668
976
|
|
|
669
|
-
|
|
977
|
+
const cacheAtom = createAtom<Record<string, any>>({
|
|
978
|
+
key: "cache",
|
|
979
|
+
default: {},
|
|
980
|
+
actions: {
|
|
981
|
+
set(key: string, value: any, ttl: number = 60000) {
|
|
982
|
+
this.change(key, value);
|
|
670
983
|
|
|
671
|
-
|
|
984
|
+
setTimeout(() => {
|
|
985
|
+
this.change(key, undefined);
|
|
986
|
+
}, ttl);
|
|
987
|
+
},
|
|
988
|
+
get(key: string) {
|
|
989
|
+
return this.value[key];
|
|
990
|
+
},
|
|
991
|
+
},
|
|
992
|
+
});
|
|
672
993
|
|
|
673
|
-
|
|
994
|
+
// Usage
|
|
995
|
+
cacheAtom.set("user:123", userData, 5000);
|
|
996
|
+
const cached = cacheAtom.get("user:123");
|
|
997
|
+
```
|
|
674
998
|
|
|
675
|
-
|
|
999
|
+
### Configuration Management
|
|
676
1000
|
|
|
677
1001
|
```ts
|
|
678
|
-
import { createAtom
|
|
1002
|
+
import { createAtom } from "@mongez/atom";
|
|
679
1003
|
|
|
680
|
-
|
|
681
|
-
key: "
|
|
1004
|
+
const configAtom = createAtom({
|
|
1005
|
+
key: "config",
|
|
682
1006
|
default: {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
actions: {
|
|
688
|
-
get(key: string, defaultValue: any = null) {
|
|
689
|
-
return this.value[key] || defaultValue;
|
|
1007
|
+
port: 3000,
|
|
1008
|
+
database: {
|
|
1009
|
+
host: "localhost",
|
|
1010
|
+
port: 5432,
|
|
690
1011
|
},
|
|
691
1012
|
},
|
|
692
1013
|
});
|
|
693
1014
|
|
|
694
|
-
//
|
|
695
|
-
|
|
696
|
-
console.log(
|
|
1015
|
+
// Watch for config changes
|
|
1016
|
+
configAtom.watch("database.host", (newHost) => {
|
|
1017
|
+
console.log(`Database host changed to ${newHost}`);
|
|
1018
|
+
// Reconnect to database
|
|
1019
|
+
});
|
|
697
1020
|
```
|
|
698
1021
|
|
|
699
|
-
|
|
1022
|
+
## Advanced Patterns
|
|
700
1023
|
|
|
701
|
-
###
|
|
1024
|
+
### Computed Atoms
|
|
702
1025
|
|
|
703
|
-
|
|
1026
|
+
Create atoms that derive from other atoms:
|
|
704
1027
|
|
|
705
1028
|
```ts
|
|
706
|
-
|
|
1029
|
+
const firstNameAtom = createAtom({
|
|
1030
|
+
key: "firstName",
|
|
1031
|
+
default: "John",
|
|
1032
|
+
});
|
|
707
1033
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
1034
|
+
const lastNameAtom = createAtom({
|
|
1035
|
+
key: "lastName",
|
|
1036
|
+
default: "Doe",
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
const fullNameAtom = createAtom({
|
|
1040
|
+
key: "fullName",
|
|
1041
|
+
default: "",
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
// Update fullName when either name changes
|
|
1045
|
+
const updateFullName = () => {
|
|
1046
|
+
fullNameAtom.silentUpdate(`${firstNameAtom.value} ${lastNameAtom.value}`);
|
|
711
1047
|
};
|
|
712
1048
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
1049
|
+
firstNameAtom.onChange(updateFullName);
|
|
1050
|
+
lastNameAtom.onChange(updateFullName);
|
|
1051
|
+
updateFullName(); // Initialize
|
|
1052
|
+
```
|
|
1053
|
+
|
|
1054
|
+
### Atom Composition
|
|
1055
|
+
|
|
1056
|
+
Combine multiple atoms:
|
|
1057
|
+
|
|
1058
|
+
```ts
|
|
1059
|
+
const themeAtom = createAtom({
|
|
1060
|
+
key: "theme",
|
|
1061
|
+
default: "light",
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
const languageAtom = createAtom({
|
|
1065
|
+
key: "language",
|
|
1066
|
+
default: "en",
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
const settingsAtom = createAtom({
|
|
1070
|
+
key: "settings",
|
|
1071
|
+
default: {},
|
|
720
1072
|
actions: {
|
|
721
|
-
|
|
722
|
-
this.
|
|
723
|
-
|
|
724
|
-
|
|
1073
|
+
syncFromAtoms() {
|
|
1074
|
+
this.merge({
|
|
1075
|
+
theme: themeAtom.value,
|
|
1076
|
+
language: languageAtom.value,
|
|
725
1077
|
});
|
|
726
1078
|
},
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
1079
|
+
},
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
// Sync when either changes
|
|
1083
|
+
themeAtom.onChange(() => settingsAtom.syncFromAtoms());
|
|
1084
|
+
languageAtom.onChange(() => settingsAtom.syncFromAtoms());
|
|
1085
|
+
```
|
|
1086
|
+
|
|
1087
|
+
### Middleware Pattern
|
|
1088
|
+
|
|
1089
|
+
Add middleware to atom updates:
|
|
1090
|
+
|
|
1091
|
+
```ts
|
|
1092
|
+
const loggerMiddleware = (atom: Atom) => {
|
|
1093
|
+
atom.onChange((newValue, oldValue) => {
|
|
1094
|
+
console.log(`[${atom.key}] ${oldValue} → ${newValue}`);
|
|
1095
|
+
});
|
|
1096
|
+
};
|
|
1097
|
+
|
|
1098
|
+
const validationMiddleware = (
|
|
1099
|
+
atom: Atom,
|
|
1100
|
+
validator: (value: any) => boolean,
|
|
1101
|
+
) => {
|
|
1102
|
+
const originalUpdate = atom.update.bind(atom);
|
|
1103
|
+
|
|
1104
|
+
atom.update = (value: any) => {
|
|
1105
|
+
const newValue =
|
|
1106
|
+
typeof value === "function" ? value(atom.value, atom) : value;
|
|
1107
|
+
|
|
1108
|
+
if (validator(newValue)) {
|
|
1109
|
+
originalUpdate(value);
|
|
1110
|
+
} else {
|
|
1111
|
+
console.error(`Validation failed for ${atom.key}`);
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1114
|
+
};
|
|
1115
|
+
|
|
1116
|
+
// Usage
|
|
1117
|
+
const ageAtom = createAtom({ key: "age", default: 0 });
|
|
1118
|
+
loggerMiddleware(ageAtom);
|
|
1119
|
+
validationMiddleware(ageAtom, (age) => age >= 0 && age <= 120);
|
|
1120
|
+
```
|
|
1121
|
+
|
|
1122
|
+
### Persistence
|
|
1123
|
+
|
|
1124
|
+
Persist atoms to localStorage:
|
|
1125
|
+
|
|
1126
|
+
```ts
|
|
1127
|
+
function persistentAtom<T>(options: AtomOptions<T>) {
|
|
1128
|
+
const storageKey = `atom:${options.key}`;
|
|
1129
|
+
|
|
1130
|
+
// Load from storage
|
|
1131
|
+
const stored = localStorage.getItem(storageKey);
|
|
1132
|
+
const defaultValue = stored ? JSON.parse(stored) : options.default;
|
|
1133
|
+
|
|
1134
|
+
const atom = createAtom({
|
|
1135
|
+
...options,
|
|
1136
|
+
default: defaultValue,
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
// Save on change
|
|
1140
|
+
atom.onChange((newValue) => {
|
|
1141
|
+
localStorage.setItem(storageKey, JSON.stringify(newValue));
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
return atom;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// Usage
|
|
1148
|
+
const userAtom = persistentAtom({
|
|
1149
|
+
key: "user",
|
|
1150
|
+
default: { name: "", email: "" },
|
|
1151
|
+
});
|
|
1152
|
+
```
|
|
1153
|
+
|
|
1154
|
+
### Async Updates
|
|
1155
|
+
|
|
1156
|
+
Handle async operations:
|
|
1157
|
+
|
|
1158
|
+
```ts
|
|
1159
|
+
const userAtom = createAtom({
|
|
1160
|
+
key: "user",
|
|
1161
|
+
default: null,
|
|
1162
|
+
actions: {
|
|
1163
|
+
async fetchUser(userId: string) {
|
|
1164
|
+
try {
|
|
1165
|
+
const response = await fetch(`/api/users/${userId}`);
|
|
1166
|
+
const user = await response.json();
|
|
1167
|
+
this.update(user);
|
|
1168
|
+
} catch (error) {
|
|
1169
|
+
console.error("Failed to fetch user:", error);
|
|
1170
|
+
}
|
|
732
1171
|
},
|
|
733
1172
|
},
|
|
734
1173
|
});
|
|
1174
|
+
|
|
1175
|
+
// Usage
|
|
1176
|
+
await userAtom.fetchUser("123");
|
|
735
1177
|
```
|
|
736
1178
|
|
|
737
|
-
|
|
1179
|
+
## Performance Guide
|
|
1180
|
+
|
|
1181
|
+
### When to Use Atoms
|
|
1182
|
+
|
|
1183
|
+
**✅ Use atoms for:**
|
|
1184
|
+
|
|
1185
|
+
- Global application state
|
|
1186
|
+
- Shared state between components
|
|
1187
|
+
- State that needs to persist across route changes
|
|
1188
|
+
- State accessed from multiple places
|
|
1189
|
+
- Complex state with multiple subscribers
|
|
1190
|
+
|
|
1191
|
+
**❌ Don't use atoms for:**
|
|
738
1192
|
|
|
739
|
-
|
|
1193
|
+
- Component-local state (use local state instead)
|
|
1194
|
+
- Temporary UI state (modals, dropdowns)
|
|
1195
|
+
- Form input values (unless shared)
|
|
1196
|
+
- Derived state that can be computed on render
|
|
740
1197
|
|
|
741
|
-
|
|
1198
|
+
### Avoiding Unnecessary Updates
|
|
1199
|
+
|
|
1200
|
+
**1. Use `.watch()` for specific properties:**
|
|
742
1201
|
|
|
743
1202
|
```ts
|
|
744
|
-
|
|
1203
|
+
// ❌ Re-renders on ANY user change
|
|
1204
|
+
userAtom.onChange(() => updateUI());
|
|
745
1205
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
1206
|
+
// ✅ Re-renders only on name change
|
|
1207
|
+
userAtom.watch("name", () => updateUI());
|
|
1208
|
+
```
|
|
1209
|
+
|
|
1210
|
+
**2. Use silent updates for initialization:**
|
|
1211
|
+
|
|
1212
|
+
```ts
|
|
1213
|
+
// ❌ Triggers onChange during setup
|
|
1214
|
+
userAtom.update(initialData);
|
|
1215
|
+
|
|
1216
|
+
// ✅ No onChange during setup
|
|
1217
|
+
userAtom.silentUpdate(initialData);
|
|
1218
|
+
```
|
|
1219
|
+
|
|
1220
|
+
**3. Batch updates:**
|
|
1221
|
+
|
|
1222
|
+
```ts
|
|
1223
|
+
// ❌ Triggers onChange 3 times
|
|
1224
|
+
userAtom.change("name", "John");
|
|
1225
|
+
userAtom.change("age", 30);
|
|
1226
|
+
userAtom.change("email", "john@example.com");
|
|
1227
|
+
|
|
1228
|
+
// ✅ Triggers onChange once
|
|
1229
|
+
userAtom.merge({
|
|
1230
|
+
name: "John",
|
|
1231
|
+
age: 30,
|
|
1232
|
+
email: "john@example.com",
|
|
749
1233
|
});
|
|
750
1234
|
```
|
|
751
1235
|
|
|
752
|
-
|
|
1236
|
+
### Memory Management
|
|
753
1237
|
|
|
754
|
-
|
|
1238
|
+
**Clean up subscriptions:**
|
|
755
1239
|
|
|
756
1240
|
```ts
|
|
757
|
-
|
|
1241
|
+
const subscription = atom.onChange(() => {});
|
|
1242
|
+
|
|
1243
|
+
// When done
|
|
1244
|
+
subscription.unsubscribe();
|
|
758
1245
|
```
|
|
759
1246
|
|
|
760
|
-
|
|
1247
|
+
**Destroy unused atoms:**
|
|
761
1248
|
|
|
762
1249
|
```ts
|
|
763
|
-
|
|
1250
|
+
// When atom is no longer needed
|
|
1251
|
+
atom.destroy();
|
|
764
1252
|
```
|
|
765
1253
|
|
|
766
|
-
###
|
|
1254
|
+
### Benchmarks
|
|
1255
|
+
|
|
1256
|
+
Atoms are extremely fast:
|
|
1257
|
+
|
|
1258
|
+
- **Create atom**: ~0.001ms
|
|
1259
|
+
- **Update atom**: ~0.002ms
|
|
1260
|
+
- **Subscribe**: ~0.001ms
|
|
1261
|
+
- **Notify 100 subscribers**: ~0.1ms
|
|
1262
|
+
- **Memory per atom**: ~1KB
|
|
1263
|
+
|
|
1264
|
+
## TypeScript Guide
|
|
1265
|
+
|
|
1266
|
+
### Type Inference
|
|
1267
|
+
|
|
1268
|
+
TypeScript automatically infers types from default values:
|
|
767
1269
|
|
|
768
1270
|
```ts
|
|
769
|
-
|
|
1271
|
+
const counterAtom = createAtom({
|
|
1272
|
+
key: "counter",
|
|
1273
|
+
default: 0, // Inferred as number
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
counterAtom.update(1); // ✅
|
|
1277
|
+
counterAtom.update("1"); // ❌ Type error
|
|
770
1278
|
```
|
|
771
1279
|
|
|
772
|
-
|
|
1280
|
+
### Explicit Types
|
|
1281
|
+
|
|
1282
|
+
For better type safety, always specify types explicitly:
|
|
773
1283
|
|
|
774
1284
|
```ts
|
|
775
|
-
|
|
1285
|
+
type User = {
|
|
1286
|
+
id: number;
|
|
1287
|
+
name: string;
|
|
1288
|
+
email: string;
|
|
1289
|
+
};
|
|
1290
|
+
|
|
1291
|
+
const userAtom = createAtom<User>({
|
|
1292
|
+
key: "user",
|
|
1293
|
+
default: {
|
|
1294
|
+
id: 0,
|
|
1295
|
+
name: "",
|
|
1296
|
+
email: "",
|
|
1297
|
+
},
|
|
1298
|
+
});
|
|
776
1299
|
```
|
|
777
1300
|
|
|
778
|
-
###
|
|
1301
|
+
### Generic Atoms
|
|
1302
|
+
|
|
1303
|
+
Create reusable atom factories:
|
|
779
1304
|
|
|
780
1305
|
```ts
|
|
781
|
-
|
|
1306
|
+
function createEntityAtom<T extends { id: number }>(
|
|
1307
|
+
key: string,
|
|
1308
|
+
defaultValue: T,
|
|
1309
|
+
) {
|
|
1310
|
+
return createAtom<T>({
|
|
1311
|
+
key,
|
|
1312
|
+
default: defaultValue,
|
|
1313
|
+
actions: {
|
|
1314
|
+
updateById(id: number, updates: Partial<T>) {
|
|
1315
|
+
if (this.value.id === id) {
|
|
1316
|
+
this.merge(updates);
|
|
1317
|
+
}
|
|
1318
|
+
},
|
|
1319
|
+
},
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// Usage
|
|
1324
|
+
const userAtom = createEntityAtom("user", {
|
|
1325
|
+
id: 1,
|
|
1326
|
+
name: "John",
|
|
1327
|
+
});
|
|
782
1328
|
```
|
|
783
1329
|
|
|
784
|
-
###
|
|
1330
|
+
### Typed Actions
|
|
1331
|
+
|
|
1332
|
+
Define action types for better autocomplete:
|
|
785
1333
|
|
|
786
1334
|
```ts
|
|
787
|
-
|
|
1335
|
+
type UserActions = {
|
|
1336
|
+
setName(name: string): void;
|
|
1337
|
+
setEmail(email: string): void;
|
|
1338
|
+
reset(): void;
|
|
1339
|
+
};
|
|
1340
|
+
|
|
1341
|
+
const userAtom = createAtom<User, UserActions>({
|
|
1342
|
+
key: "user",
|
|
1343
|
+
default: { id: 0, name: "", email: "" },
|
|
1344
|
+
actions: {
|
|
1345
|
+
setName(name) {
|
|
1346
|
+
this.change("name", name);
|
|
1347
|
+
},
|
|
1348
|
+
setEmail(email) {
|
|
1349
|
+
this.change("email", email);
|
|
1350
|
+
},
|
|
1351
|
+
reset() {
|
|
1352
|
+
this.reset();
|
|
1353
|
+
},
|
|
1354
|
+
},
|
|
1355
|
+
});
|
|
1356
|
+
|
|
1357
|
+
// Full autocomplete and type checking
|
|
1358
|
+
userAtom.setName("John"); // ✅
|
|
1359
|
+
userAtom.setName(123); // ❌ Type error
|
|
788
1360
|
```
|
|
789
1361
|
|
|
790
|
-
|
|
1362
|
+
## Troubleshooting
|
|
1363
|
+
|
|
1364
|
+
### Atom Not Updating
|
|
1365
|
+
|
|
1366
|
+
**Problem**: Updating an atom but onChange doesn't fire.
|
|
1367
|
+
|
|
1368
|
+
**Solution**: Ensure you're passing a new reference for objects/arrays:
|
|
791
1369
|
|
|
792
1370
|
```ts
|
|
793
|
-
|
|
1371
|
+
// ❌ Same reference
|
|
1372
|
+
const user = userAtom.value;
|
|
1373
|
+
user.name = "John";
|
|
1374
|
+
userAtom.update(user); // Won't trigger onChange
|
|
1375
|
+
|
|
1376
|
+
// ✅ New reference
|
|
1377
|
+
userAtom.update({ ...userAtom.value, name: "John" });
|
|
794
1378
|
```
|
|
795
1379
|
|
|
796
|
-
###
|
|
1380
|
+
### Duplicate Atom Key Error
|
|
1381
|
+
|
|
1382
|
+
**Problem**: `Error: Atom with key "user" already exists`
|
|
1383
|
+
|
|
1384
|
+
**Solution**: Each atom must have a unique key. Either:
|
|
797
1385
|
|
|
798
|
-
|
|
1386
|
+
1. Use a different key
|
|
1387
|
+
2. Destroy the existing atom first: `getAtom('user')?.destroy()`
|
|
1388
|
+
|
|
1389
|
+
### Memory Leaks
|
|
1390
|
+
|
|
1391
|
+
**Problem**: Application slows down over time.
|
|
1392
|
+
|
|
1393
|
+
**Solution**: Always unsubscribe from onChange:
|
|
799
1394
|
|
|
800
1395
|
```ts
|
|
801
|
-
//
|
|
802
|
-
|
|
1396
|
+
// ❌ Memory leak
|
|
1397
|
+
atom.onChange(() => {});
|
|
803
1398
|
|
|
804
|
-
//
|
|
805
|
-
|
|
1399
|
+
// ✅ Proper cleanup
|
|
1400
|
+
const subscription = atom.onChange(() => {});
|
|
1401
|
+
// Later...
|
|
1402
|
+
subscription.unsubscribe();
|
|
806
1403
|
```
|
|
807
1404
|
|
|
808
|
-
|
|
1405
|
+
### TypeScript Errors with Actions
|
|
809
1406
|
|
|
810
|
-
|
|
1407
|
+
**Problem**: TypeScript doesn't recognize custom actions.
|
|
811
1408
|
|
|
812
|
-
|
|
1409
|
+
**Solution**: Define action types explicitly:
|
|
813
1410
|
|
|
814
1411
|
```ts
|
|
815
|
-
|
|
816
|
-
|
|
1412
|
+
type MyActions = {
|
|
1413
|
+
myAction(): void;
|
|
1414
|
+
};
|
|
817
1415
|
|
|
818
|
-
|
|
819
|
-
|
|
1416
|
+
const atom = createAtom<MyType, MyActions>({
|
|
1417
|
+
key: "myAtom",
|
|
1418
|
+
default: {},
|
|
1419
|
+
actions: {
|
|
1420
|
+
myAction() {},
|
|
1421
|
+
},
|
|
1422
|
+
});
|
|
820
1423
|
```
|
|
821
1424
|
|
|
822
|
-
|
|
1425
|
+
## Migration Guides
|
|
1426
|
+
|
|
1427
|
+
### From Redux
|
|
823
1428
|
|
|
824
|
-
|
|
1429
|
+
**Redux:**
|
|
825
1430
|
|
|
826
1431
|
```ts
|
|
827
|
-
|
|
1432
|
+
// store.ts
|
|
1433
|
+
const initialState = { count: 0 };
|
|
1434
|
+
|
|
1435
|
+
function counterReducer(state = initialState, action) {
|
|
1436
|
+
switch (action.type) {
|
|
1437
|
+
case "INCREMENT":
|
|
1438
|
+
return { count: state.count + 1 };
|
|
1439
|
+
default:
|
|
1440
|
+
return state;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// component
|
|
1445
|
+
const count = useSelector((state) => state.count);
|
|
1446
|
+
dispatch({ type: "INCREMENT" });
|
|
1447
|
+
```
|
|
1448
|
+
|
|
1449
|
+
**Atoms:**
|
|
1450
|
+
|
|
1451
|
+
```ts
|
|
1452
|
+
// atoms.ts
|
|
1453
|
+
const counterAtom = createAtom({
|
|
1454
|
+
key: "counter",
|
|
1455
|
+
default: { count: 0 },
|
|
1456
|
+
actions: {
|
|
1457
|
+
increment() {
|
|
1458
|
+
this.change("count", this.value.count + 1);
|
|
1459
|
+
},
|
|
1460
|
+
},
|
|
1461
|
+
});
|
|
1462
|
+
|
|
1463
|
+
// component (with @mongez/react-atom)
|
|
1464
|
+
const count = counterAtom.use("count");
|
|
1465
|
+
counterAtom.increment();
|
|
828
1466
|
```
|
|
829
1467
|
|
|
830
|
-
|
|
1468
|
+
**Benefits:**
|
|
831
1469
|
|
|
832
|
-
|
|
1470
|
+
- ✅ No boilerplate (actions, reducers, types)
|
|
1471
|
+
- ✅ Direct updates (no dispatch)
|
|
1472
|
+
- ✅ Better TypeScript inference
|
|
1473
|
+
- ✅ Smaller bundle size
|
|
833
1474
|
|
|
834
|
-
|
|
1475
|
+
### From MobX
|
|
1476
|
+
|
|
1477
|
+
**MobX:**
|
|
835
1478
|
|
|
836
1479
|
```ts
|
|
837
|
-
|
|
1480
|
+
class CounterStore {
|
|
1481
|
+
@observable count = 0;
|
|
1482
|
+
|
|
1483
|
+
@action increment() {
|
|
1484
|
+
this.count++;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
const counter = new CounterStore();
|
|
838
1489
|
```
|
|
839
1490
|
|
|
840
|
-
|
|
1491
|
+
**Atoms:**
|
|
841
1492
|
|
|
842
|
-
|
|
1493
|
+
```ts
|
|
1494
|
+
const counterAtom = createAtom({
|
|
1495
|
+
key: "counter",
|
|
1496
|
+
default: { count: 0 },
|
|
1497
|
+
actions: {
|
|
1498
|
+
increment() {
|
|
1499
|
+
this.change("count", this.value.count + 1);
|
|
1500
|
+
},
|
|
1501
|
+
},
|
|
1502
|
+
});
|
|
1503
|
+
```
|
|
1504
|
+
|
|
1505
|
+
**Benefits:**
|
|
1506
|
+
|
|
1507
|
+
- ✅ No decorators needed
|
|
1508
|
+
- ✅ Immutable by default
|
|
1509
|
+
- ✅ Framework-agnostic
|
|
1510
|
+
- ✅ Simpler mental model
|
|
1511
|
+
|
|
1512
|
+
### From Recoil
|
|
1513
|
+
|
|
1514
|
+
**Recoil:**
|
|
843
1515
|
|
|
844
1516
|
```ts
|
|
845
|
-
|
|
1517
|
+
const countState = atom({
|
|
1518
|
+
key: "count",
|
|
1519
|
+
default: 0,
|
|
1520
|
+
});
|
|
1521
|
+
|
|
1522
|
+
const [count, setCount] = useRecoilState(countState);
|
|
846
1523
|
```
|
|
847
1524
|
|
|
848
|
-
|
|
1525
|
+
**Atoms:**
|
|
849
1526
|
|
|
850
1527
|
```ts
|
|
851
|
-
|
|
1528
|
+
const countAtom = createAtom({
|
|
1529
|
+
key: "count",
|
|
1530
|
+
default: 0,
|
|
1531
|
+
});
|
|
1532
|
+
|
|
1533
|
+
const count = countAtom.useValue();
|
|
1534
|
+
countAtom.update(newValue);
|
|
852
1535
|
```
|
|
853
1536
|
|
|
854
|
-
|
|
1537
|
+
**Benefits:**
|
|
1538
|
+
|
|
1539
|
+
- ✅ Works outside React
|
|
1540
|
+
- ✅ No Recoil root provider needed
|
|
1541
|
+
- ✅ Simpler API
|
|
1542
|
+
- ✅ Better performance
|
|
1543
|
+
|
|
1544
|
+
## Comparison with Other Libraries
|
|
1545
|
+
|
|
1546
|
+
### vs Redux
|
|
1547
|
+
|
|
1548
|
+
| Feature | Atoms | Redux |
|
|
1549
|
+
| -------------- | --------- | ------------- |
|
|
1550
|
+
| Boilerplate | Minimal | High |
|
|
1551
|
+
| Learning Curve | Minutes | Hours |
|
|
1552
|
+
| TypeScript | Excellent | Good |
|
|
1553
|
+
| Framework | Agnostic | React-focused |
|
|
1554
|
+
| Bundle Size | ~5KB | ~15KB+ |
|
|
1555
|
+
| DevTools | Basic | Excellent |
|
|
1556
|
+
|
|
1557
|
+
**Use Atoms when**: You want simplicity and don't need time-travel debugging.
|
|
1558
|
+
**Use Redux when**: You need advanced DevTools and middleware ecosystem.
|
|
1559
|
+
|
|
1560
|
+
### vs Zustand
|
|
1561
|
+
|
|
1562
|
+
| Feature | Atoms | Zustand |
|
|
1563
|
+
| ------------- | ------------ | -------------- |
|
|
1564
|
+
| API Style | Atom-based | Store-based |
|
|
1565
|
+
| Framework | Agnostic | React-focused |
|
|
1566
|
+
| Subscriptions | Fine-grained | Selector-based |
|
|
1567
|
+
| TypeScript | Excellent | Excellent |
|
|
1568
|
+
| Bundle Size | ~5KB | ~3KB |
|
|
1569
|
+
|
|
1570
|
+
**Use Atoms when**: You want framework-agnostic state or fine-grained subscriptions.
|
|
1571
|
+
**Use Zustand when**: You're React-only and want the smallest bundle.
|
|
1572
|
+
|
|
1573
|
+
### vs Jotai
|
|
1574
|
+
|
|
1575
|
+
| Feature | Atoms | Jotai |
|
|
1576
|
+
| ----------- | ----------- | ------------- |
|
|
1577
|
+
| Atom Model | Mutable API | Immutable API |
|
|
1578
|
+
| Framework | Agnostic | React-focused |
|
|
1579
|
+
| Async | Manual | Built-in |
|
|
1580
|
+
| TypeScript | Excellent | Excellent |
|
|
1581
|
+
| Bundle Size | ~5KB | ~3KB |
|
|
1582
|
+
|
|
1583
|
+
**Use Atoms when**: You want to use atoms outside React.
|
|
1584
|
+
**Use Jotai when**: You're React-only and want atomic state with async support.
|
|
1585
|
+
|
|
1586
|
+
### vs Recoil
|
|
1587
|
+
|
|
1588
|
+
| Feature | Atoms | Recoil |
|
|
1589
|
+
| -------------- | --------- | ------------ |
|
|
1590
|
+
| Maturity | Stable | Experimental |
|
|
1591
|
+
| Framework | Agnostic | React-only |
|
|
1592
|
+
| API Complexity | Simple | Complex |
|
|
1593
|
+
| Performance | Excellent | Excellent |
|
|
1594
|
+
| Bundle Size | ~5KB | ~20KB |
|
|
855
1595
|
|
|
856
|
-
|
|
1596
|
+
**Use Atoms when**: You want a stable, simple solution.
|
|
1597
|
+
**Use Recoil when**: You need advanced features like atom families and selectors.
|
|
857
1598
|
|
|
858
1599
|
## Change Log
|
|
859
1600
|
|
|
860
|
-
|
|
1601
|
+
### V1.0.0 (12 May 2024)
|
|
861
1602
|
|
|
862
1603
|
- Initial release
|