atomirx 0.0.7 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +198 -2234
- package/bin/cli.js +90 -0
- package/dist/core/derived.d.ts +2 -2
- package/dist/core/effect.d.ts +3 -2
- package/dist/core/onCreateHook.d.ts +15 -2
- package/dist/core/onErrorHook.d.ts +4 -1
- package/dist/core/pool.d.ts +78 -0
- package/dist/core/pool.test.d.ts +1 -0
- package/dist/core/select-boolean.test.d.ts +1 -0
- package/dist/core/select-pool.test.d.ts +1 -0
- package/dist/core/select.d.ts +278 -86
- package/dist/core/types.d.ts +233 -1
- package/dist/core/withAbort.d.ts +95 -0
- package/dist/core/withReady.d.ts +3 -3
- package/dist/devtools/constants.d.ts +41 -0
- package/dist/devtools/index.cjs +1 -0
- package/dist/devtools/index.d.ts +29 -0
- package/dist/devtools/index.js +429 -0
- package/dist/devtools/registry.d.ts +98 -0
- package/dist/devtools/registry.test.d.ts +1 -0
- package/dist/devtools/setup.d.ts +61 -0
- package/dist/devtools/types.d.ts +311 -0
- package/dist/index-BZEnfIcB.cjs +1 -0
- package/dist/index-BbPZhsDl.js +1653 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +18 -14
- package/dist/onDispatchHook-C8yLzr-o.cjs +1 -0
- package/dist/onDispatchHook-SKbiIUaJ.js +5 -0
- package/dist/onErrorHook-BGGy3tqK.js +38 -0
- package/dist/onErrorHook-DHBASmYw.cjs +1 -0
- package/dist/react/index.cjs +1 -30
- package/dist/react/index.js +206 -791
- package/dist/react/onDispatchHook.d.ts +106 -0
- package/dist/react/useAction.d.ts +4 -1
- package/dist/react-devtools/DevToolsPanel.d.ts +93 -0
- package/dist/react-devtools/EntityDetails.d.ts +10 -0
- package/dist/react-devtools/EntityList.d.ts +15 -0
- package/dist/react-devtools/LogList.d.ts +12 -0
- package/dist/react-devtools/hooks.d.ts +50 -0
- package/dist/react-devtools/index.cjs +1 -0
- package/dist/react-devtools/index.d.ts +31 -0
- package/dist/react-devtools/index.js +1589 -0
- package/dist/react-devtools/styles.d.ts +148 -0
- package/package.json +26 -2
- package/skills/atomirx/SKILL.md +456 -0
- package/skills/atomirx/references/async-patterns.md +188 -0
- package/skills/atomirx/references/atom-patterns.md +238 -0
- package/skills/atomirx/references/deferred-loading.md +191 -0
- package/skills/atomirx/references/derived-patterns.md +428 -0
- package/skills/atomirx/references/effect-patterns.md +426 -0
- package/skills/atomirx/references/error-handling.md +140 -0
- package/skills/atomirx/references/hooks.md +322 -0
- package/skills/atomirx/references/pool-patterns.md +229 -0
- package/skills/atomirx/references/react-integration.md +411 -0
- package/skills/atomirx/references/rules.md +407 -0
- package/skills/atomirx/references/select-context.md +309 -0
- package/skills/atomirx/references/service-template.md +172 -0
- package/skills/atomirx/references/store-template.md +205 -0
- package/skills/atomirx/references/testing-patterns.md +431 -0
- package/coverage/base.css +0 -224
- package/coverage/block-navigation.js +0 -87
- package/coverage/clover.xml +0 -1440
- package/coverage/coverage-final.json +0 -14
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +0 -131
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -210
- package/coverage/src/core/atom.ts.html +0 -889
- package/coverage/src/core/batch.ts.html +0 -223
- package/coverage/src/core/define.ts.html +0 -805
- package/coverage/src/core/emitter.ts.html +0 -919
- package/coverage/src/core/equality.ts.html +0 -631
- package/coverage/src/core/hook.ts.html +0 -460
- package/coverage/src/core/index.html +0 -281
- package/coverage/src/core/isAtom.ts.html +0 -100
- package/coverage/src/core/isPromiseLike.ts.html +0 -133
- package/coverage/src/core/onCreateHook.ts.html +0 -138
- package/coverage/src/core/scheduleNotifyHook.ts.html +0 -94
- package/coverage/src/core/types.ts.html +0 -523
- package/coverage/src/core/withUse.ts.html +0 -253
- package/coverage/src/index.html +0 -116
- package/coverage/src/index.ts.html +0 -106
- package/dist/index-CBVj1kSj.js +0 -1350
- package/dist/index-Cxk9v0um.cjs +0 -1
- package/scripts/publish.js +0 -198
- package/src/core/atom.test.ts +0 -633
- package/src/core/atom.ts +0 -311
- package/src/core/atomState.test.ts +0 -342
- package/src/core/atomState.ts +0 -256
- package/src/core/batch.test.ts +0 -257
- package/src/core/batch.ts +0 -172
- package/src/core/define.test.ts +0 -343
- package/src/core/define.ts +0 -243
- package/src/core/derived.test.ts +0 -1215
- package/src/core/derived.ts +0 -450
- package/src/core/effect.test.ts +0 -802
- package/src/core/effect.ts +0 -188
- package/src/core/emitter.test.ts +0 -364
- package/src/core/emitter.ts +0 -392
- package/src/core/equality.test.ts +0 -392
- package/src/core/equality.ts +0 -182
- package/src/core/getAtomState.ts +0 -69
- package/src/core/hook.test.ts +0 -227
- package/src/core/hook.ts +0 -177
- package/src/core/isAtom.ts +0 -27
- package/src/core/isPromiseLike.test.ts +0 -72
- package/src/core/isPromiseLike.ts +0 -16
- package/src/core/onCreateHook.ts +0 -107
- package/src/core/onErrorHook.test.ts +0 -350
- package/src/core/onErrorHook.ts +0 -52
- package/src/core/promiseCache.test.ts +0 -241
- package/src/core/promiseCache.ts +0 -284
- package/src/core/scheduleNotifyHook.ts +0 -53
- package/src/core/select.ts +0 -729
- package/src/core/selector.test.ts +0 -799
- package/src/core/types.ts +0 -389
- package/src/core/withReady.test.ts +0 -534
- package/src/core/withReady.ts +0 -191
- package/src/core/withUse.test.ts +0 -249
- package/src/core/withUse.ts +0 -56
- package/src/index.test.ts +0 -80
- package/src/index.ts +0 -65
- package/src/react/index.ts +0 -21
- package/src/react/rx.test.tsx +0 -571
- package/src/react/rx.tsx +0 -531
- package/src/react/strictModeTest.tsx +0 -71
- package/src/react/useAction.test.ts +0 -987
- package/src/react/useAction.ts +0 -607
- package/src/react/useSelector.test.ts +0 -182
- package/src/react/useSelector.ts +0 -292
- package/src/react/useStable.test.ts +0 -553
- package/src/react/useStable.ts +0 -288
- package/tsconfig.json +0 -9
- package/v2.md +0 -725
- package/vite.config.ts +0 -39
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
# Derived Patterns
|
|
2
|
+
|
|
3
|
+
## .get() Returns Promise, But Execution Can Be Sync
|
|
4
|
+
|
|
5
|
+
`.get()` **ALWAYS returns `Promise<T>`** — this is the API contract.
|
|
6
|
+
|
|
7
|
+
**BUT** internally, computation is sync or async depending on dependencies:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
const count$ = atom(0); // Sync value
|
|
11
|
+
const user$ = atom(fetchUser()); // Async value (Promise)
|
|
12
|
+
|
|
13
|
+
const doubled$ = derived(({ read }) => read(count$) * 2);
|
|
14
|
+
const greeting$ = derived(({ read }) => `Hello, ${read(user$).name}`);
|
|
15
|
+
|
|
16
|
+
// API always returns Promise
|
|
17
|
+
await doubled$.get(); // Promise<number>
|
|
18
|
+
await greeting$.get(); // Promise<string>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Why This Matters: Sync Mental Model in UI
|
|
22
|
+
|
|
23
|
+
**In SelectContext (useSelector, derived, effect):**
|
|
24
|
+
|
|
25
|
+
- Everything runs **sync** — no awaits in your code
|
|
26
|
+
- Suspense handles async dependencies automatically
|
|
27
|
+
- You don't care if dependency is sync/async
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
const doubled$ = derived(({ read }) => read(count$) * 2);
|
|
31
|
+
|
|
32
|
+
function MyComponent() {
|
|
33
|
+
// Always sync execution — no Promise throw if count$ is sync
|
|
34
|
+
const doubled = useSelector(doubled$);
|
|
35
|
+
|
|
36
|
+
// Even with async dependency — still sync code, Suspense handles it
|
|
37
|
+
const user = useSelector(user$);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div>
|
|
41
|
+
{doubled} - {user.name}
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Wrap with Suspense to handle async deps
|
|
47
|
+
<Suspense fallback={<Loading />}>
|
|
48
|
+
<MyComponent />
|
|
49
|
+
</Suspense>;
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Effects work the same way:**
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
effect(({ read }) => {
|
|
56
|
+
const count = read(count$); // Sync
|
|
57
|
+
const user = read(user$); // Async — effect waits, no await needed
|
|
58
|
+
console.log(count, user.name);
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Outside SelectContext: Use await
|
|
63
|
+
|
|
64
|
+
Services, utilities, event handlers — use `.get()` with await:
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
// In service/utility
|
|
68
|
+
async function processData() {
|
|
69
|
+
const user = await user$.get(); // Await because outside selector
|
|
70
|
+
return transform(user);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// In event handler
|
|
74
|
+
const handleClick = async () => {
|
|
75
|
+
const count = await count$.get();
|
|
76
|
+
sendAnalytics(count);
|
|
77
|
+
};
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Do Utils Need Sync Values?
|
|
81
|
+
|
|
82
|
+
**No.** If a utility needs sync value, it should:
|
|
83
|
+
|
|
84
|
+
1. Accept the value as parameter (not atom)
|
|
85
|
+
2. Read from atom that contains sync value directly
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
// ❌ Don't make utils depend on atoms
|
|
89
|
+
function formatCount() {
|
|
90
|
+
return count$.get(); // Returns Promise, awkward
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ✅ Accept value as parameter
|
|
94
|
+
function formatCount(count: number) {
|
|
95
|
+
return `Count: ${count}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Usage in selector
|
|
99
|
+
useSelector(({ read }) => formatCount(read(count$)));
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Summary
|
|
103
|
+
|
|
104
|
+
| Context | Execution | Async Handling |
|
|
105
|
+
| ---------------------------- | ---------- | -------------- |
|
|
106
|
+
| useSelector/derived/effect | Sync code | Suspense/wait |
|
|
107
|
+
| Outside (services, handlers) | Async code | await .get() |
|
|
108
|
+
|
|
109
|
+
## When to Use
|
|
110
|
+
|
|
111
|
+
**Use `derived()` for:**
|
|
112
|
+
|
|
113
|
+
- Combining/transforming atoms
|
|
114
|
+
- Computed values that auto-update
|
|
115
|
+
- Handling sync + async atoms uniformly
|
|
116
|
+
|
|
117
|
+
**NEVER use for:**
|
|
118
|
+
|
|
119
|
+
- **Updating atoms** — use `effect()`
|
|
120
|
+
- Side effects — use `effect()`
|
|
121
|
+
- User actions — use plain functions
|
|
122
|
+
|
|
123
|
+
## Core API
|
|
124
|
+
|
|
125
|
+
| Property/Method | Signature | Description |
|
|
126
|
+
| --------------- | ------------------- | ------------------------ |
|
|
127
|
+
| `get()` | `() => Promise<T>` | Get computed value |
|
|
128
|
+
| `staleValue` | `T \| undefined` | Last resolved / fallback |
|
|
129
|
+
| `state()` | `() => AtomState` | State without throwing |
|
|
130
|
+
| `refresh()` | `() => void` | Force recomputation |
|
|
131
|
+
| `on()` | `(listener) => sub` | Subscribe |
|
|
132
|
+
|
|
133
|
+
## Selector Rules (CRITICAL)
|
|
134
|
+
|
|
135
|
+
### NEVER Update Atoms
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
// ❌ FORBIDDEN
|
|
139
|
+
derived(({ read }) => {
|
|
140
|
+
const items = read(cartItems$);
|
|
141
|
+
cartTotal$.set(items.reduce((s, i) => s + i.price, 0)); // NEVER
|
|
142
|
+
return total;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// ✅ Use effect()
|
|
146
|
+
effect(
|
|
147
|
+
({ read }) => {
|
|
148
|
+
const items = read(cartItems$);
|
|
149
|
+
cartTotal$.set(items.reduce((s, i) => s + i.price, 0));
|
|
150
|
+
},
|
|
151
|
+
{ meta: { key: "compute.cartTotal" } }
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// ✅ Or compute in derived
|
|
155
|
+
const cartTotal$ = derived(({ read }) =>
|
|
156
|
+
read(cartItems$).reduce((s, i) => s + i.price, 0)
|
|
157
|
+
);
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### MUST Return Sync Value
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
// ❌ FORBIDDEN
|
|
164
|
+
derived(async ({ read }) => await fetch("/api"));
|
|
165
|
+
derived(({ read }) => fetch("/api").then((r) => r.json()));
|
|
166
|
+
|
|
167
|
+
// ✅ REQUIRED
|
|
168
|
+
const data$ = atom(fetch("/api").then((r) => r.json()));
|
|
169
|
+
derived(({ read }) => read(data$));
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### NEVER try/catch — Use safe()
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// ❌ FORBIDDEN — catches Promise
|
|
176
|
+
derived(({ read }) => {
|
|
177
|
+
try {
|
|
178
|
+
return read(asyncAtom$);
|
|
179
|
+
} catch (e) {
|
|
180
|
+
return "fallback";
|
|
181
|
+
} // Breaks Suspense
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// ✅ REQUIRED
|
|
185
|
+
derived(({ read, safe }) => {
|
|
186
|
+
const [err, data] = safe(() => read(asyncAtom$));
|
|
187
|
+
if (err) return "error fallback";
|
|
188
|
+
return data;
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## staleValue
|
|
193
|
+
|
|
194
|
+
Last resolved value, or fallback if not yet resolved / error occurred.
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
// Async atom dependency
|
|
198
|
+
const user$ = atom(fetchUser()); // Promise<User>
|
|
199
|
+
|
|
200
|
+
// Without fallback
|
|
201
|
+
const userName$ = derived(({ read }) => read(user$).name);
|
|
202
|
+
userName$.staleValue; // undefined — async not resolved yet
|
|
203
|
+
await userName$.get();
|
|
204
|
+
userName$.staleValue; // "John" — last resolved value
|
|
205
|
+
|
|
206
|
+
// With fallback — used when:
|
|
207
|
+
// 1. Async dependency not resolved yet
|
|
208
|
+
// 2. Computation error (async rejects or transform fails)
|
|
209
|
+
const userPosts$ = derived(({ read }) => read(userPostsAsync$).length, {
|
|
210
|
+
fallback: 0,
|
|
211
|
+
});
|
|
212
|
+
userPosts$.staleValue; // 0 — async not resolved yet
|
|
213
|
+
await userPosts$.get();
|
|
214
|
+
userPosts$.staleValue; // 42 — resolved value
|
|
215
|
+
|
|
216
|
+
// If async rejects or computation throws
|
|
217
|
+
userPosts$.staleValue; // 0 — fallback used on error
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
| Scenario | Without fallback | With fallback |
|
|
221
|
+
| -------------- | ---------------- | -------------- |
|
|
222
|
+
| Before resolve | `undefined` | fallback value |
|
|
223
|
+
| After resolve | Last value | Last value |
|
|
224
|
+
| On error | `undefined` | fallback value |
|
|
225
|
+
|
|
226
|
+
### Show Cached While Loading
|
|
227
|
+
|
|
228
|
+
```tsx
|
|
229
|
+
function PostCount() {
|
|
230
|
+
const state = useSelector(({ state }) => state(postCount$));
|
|
231
|
+
const stale = postCount$.staleValue;
|
|
232
|
+
|
|
233
|
+
if (state.status === "loading")
|
|
234
|
+
return <div className="loading">{stale ?? "..."}</div>;
|
|
235
|
+
return <div>{state.value}</div>;
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## state()
|
|
240
|
+
|
|
241
|
+
Get state without Suspense:
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
data$.state();
|
|
245
|
+
// { status: "ready", value: T }
|
|
246
|
+
// { status: "error", error: unknown }
|
|
247
|
+
// { status: "loading", promise: Promise<T> }
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## refresh()
|
|
251
|
+
|
|
252
|
+
Force recomputation:
|
|
253
|
+
|
|
254
|
+
```tsx
|
|
255
|
+
function DataList() {
|
|
256
|
+
const stable = useStable({ onRefresh: () => data$.refresh() });
|
|
257
|
+
return (
|
|
258
|
+
<PullToRefresh onRefresh={stable.onRefresh}>
|
|
259
|
+
<List />
|
|
260
|
+
</PullToRefresh>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Options
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
interface DerivedOptions<T> {
|
|
269
|
+
meta?: { key?: string };
|
|
270
|
+
equals?: Equality<T>;
|
|
271
|
+
fallback?: T;
|
|
272
|
+
onError?: (error: unknown) => void;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Shallow equality
|
|
276
|
+
const user$ = derived(({ read }) => ({ ...read(userData$) }), {
|
|
277
|
+
equals: "shallow",
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// Custom equality
|
|
281
|
+
const data$ = derived(({ read }) => read(source$), {
|
|
282
|
+
equals: (a, b) => a.id === b.id,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Error callback
|
|
286
|
+
const risky$ = derived(({ read }) => JSON.parse(read(raw$)), {
|
|
287
|
+
onError: (err) => Sentry.captureException(err),
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## Common Patterns
|
|
292
|
+
|
|
293
|
+
### Computed Property
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
const fullName$ = derived(({ read }) => {
|
|
297
|
+
const { firstName, lastName } = read(user$);
|
|
298
|
+
return `${firstName} ${lastName}`;
|
|
299
|
+
});
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Filtered List
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
const filteredTodos$ = derived(({ read }) => {
|
|
306
|
+
const todos = read(todos$);
|
|
307
|
+
const filter = read(filter$);
|
|
308
|
+
switch (filter) {
|
|
309
|
+
case "active":
|
|
310
|
+
return todos.filter((t) => !t.completed);
|
|
311
|
+
case "completed":
|
|
312
|
+
return todos.filter((t) => t.completed);
|
|
313
|
+
default:
|
|
314
|
+
return todos;
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Combined Async
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
const dashboard$ = derived(({ all }) => {
|
|
323
|
+
const [user, posts, notifications] = all([user$, posts$, notifications$]);
|
|
324
|
+
return { user, posts, notifications };
|
|
325
|
+
});
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Conditional Dependencies
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
const data$ = derived(({ read, and }) => {
|
|
332
|
+
if (!and([isLoggedIn$, hasPermission$])) return null;
|
|
333
|
+
return read(expensiveData$);
|
|
334
|
+
});
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Error-Resilient
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
const dashboard$ = derived(({ settled }) => {
|
|
341
|
+
const [userR, postsR, statsR] = settled([user$, posts$, stats$]);
|
|
342
|
+
return {
|
|
343
|
+
user: userR.status === "ready" ? userR.value : null,
|
|
344
|
+
posts: postsR.status === "ready" ? postsR.value : [],
|
|
345
|
+
errors: [userR, postsR, statsR]
|
|
346
|
+
.filter((r) => r.status === "error")
|
|
347
|
+
.map((r) => r.error),
|
|
348
|
+
};
|
|
349
|
+
});
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Race Cache vs API
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
const article$ = derived(({ race }) => {
|
|
356
|
+
const result = race({ cache: cachedArticle$, network: fetchedArticle$ });
|
|
357
|
+
console.log(`From: ${result.key}`);
|
|
358
|
+
return result.value;
|
|
359
|
+
});
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### From Pool
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
const currentUser$ = derived(({ read, ready, from }) => {
|
|
366
|
+
const userId = ready(currentUserId$);
|
|
367
|
+
const user$ = from(userPool, userId);
|
|
368
|
+
return read(user$);
|
|
369
|
+
});
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Non-Reactive Config (untrack)
|
|
373
|
+
|
|
374
|
+
Read values without creating dependencies — useful for config or initial values that shouldn't trigger re-computation:
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
// Config doesn't change often — don't re-compute when it does
|
|
378
|
+
const formatted$ = derived(({ read, untrack }) => {
|
|
379
|
+
const data = read(data$); // Re-compute when data changes
|
|
380
|
+
const format = untrack(format$); // DON'T re-compute when format changes
|
|
381
|
+
return formatData(data, format);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Snapshot multiple atoms at once
|
|
385
|
+
const snapshot$ = derived(({ read, untrack }) => {
|
|
386
|
+
const trigger = read(trigger$); // Only re-compute on trigger
|
|
387
|
+
return untrack(() => ({
|
|
388
|
+
a: read(a$),
|
|
389
|
+
b: read(b$),
|
|
390
|
+
c: read(c$),
|
|
391
|
+
timestamp: Date.now(),
|
|
392
|
+
}));
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// Initial value pattern
|
|
396
|
+
const counter$ = derived(({ read, untrack }) => {
|
|
397
|
+
const count = read(count$);
|
|
398
|
+
const initial = untrack(initialValue$); // Don't re-run if initial changes
|
|
399
|
+
return count - initial;
|
|
400
|
+
});
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
| Use Case | Method |
|
|
404
|
+
| -------- | ------ |
|
|
405
|
+
| Value that triggers re-compute | `read()` |
|
|
406
|
+
| Config/settings rarely changing | `untrack()` |
|
|
407
|
+
| Snapshot of multiple atoms | `untrack(() => ...)` |
|
|
408
|
+
| Initial/reference values | `untrack()` |
|
|
409
|
+
|
|
410
|
+
## Effect vs Derived
|
|
411
|
+
|
|
412
|
+
| Aspect | derived() | effect() |
|
|
413
|
+
| ------------ | ----------------- | ------------------- |
|
|
414
|
+
| Returns | `Promise<T>` | void |
|
|
415
|
+
| Execution | Lazy (on access) | Eager (immediately) |
|
|
416
|
+
| Purpose | Transform/combine | Sync, persist, log |
|
|
417
|
+
| **Can set** | **❌ NEVER** | **✅ Yes** |
|
|
418
|
+
| Subscription | When accessed | Always active |
|
|
419
|
+
|
|
420
|
+
## When to Use What
|
|
421
|
+
|
|
422
|
+
| Scenario | Solution |
|
|
423
|
+
| -------------------------------- | -------------- |
|
|
424
|
+
| User clicks → modify atoms | Plain function |
|
|
425
|
+
| React to changes → compute value | `derived()` |
|
|
426
|
+
| React to changes → side effects | `effect()` |
|
|
427
|
+
| Combine multiple atoms | `derived()` |
|
|
428
|
+
| Persist/log/sync on changes | `effect()` |
|