atomirx 0.0.8 → 0.1.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.
- 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 -1
- package/dist/react/index.js +191 -151
- 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 -42
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
# React Integration
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
| Hook | Purpose | Subscription |
|
|
6
|
+
| ------------- | ---------------------------- | ------------ |
|
|
7
|
+
| `useSelector` | Read atoms, auto re-render | Yes |
|
|
8
|
+
| `rx` | Inline reactive components | Yes |
|
|
9
|
+
| `useAction` | Async ops with state | No (manual) |
|
|
10
|
+
| `useStable` | Stable refs for callbacks | No |
|
|
11
|
+
|
|
12
|
+
## useSelector
|
|
13
|
+
|
|
14
|
+
### CRITICAL: One useSelector Per Component
|
|
15
|
+
|
|
16
|
+
`useSelector` is **powerful** — handles complex expressions, multiple atoms, computed values. **Almost never need multiple calls.**
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
// ✅ REQUIRED — Single useSelector, complex selection
|
|
20
|
+
const { count, user, settings, isAdmin, cartTotal } = useSelector(({ read, use }) => ({
|
|
21
|
+
count: read(count$),
|
|
22
|
+
user: read(user$),
|
|
23
|
+
settings: read(settings$),
|
|
24
|
+
isAdmin: read(user$)?.role === "admin",
|
|
25
|
+
cartTotal: use(selectCartTotal),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
// ❌ FORBIDDEN — Multiple useSelectors
|
|
29
|
+
const count = useSelector(count$);
|
|
30
|
+
const user = useSelector(user$);
|
|
31
|
+
const settings = useSelector(settings$);
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Rare Exception: Hook Dependencies
|
|
35
|
+
|
|
36
|
+
Only use multiple `useSelector` when a selected value must be passed to another hook:
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
// ✅ OK — Rare case: hook needs selected value
|
|
40
|
+
function useProductDetails() {
|
|
41
|
+
const productId = useSelector(({ read }) => read(currentProductId$));
|
|
42
|
+
const analytics = useAnalyticsHook(productId); // Hook needs productId
|
|
43
|
+
const { product, reviews } = useSelector(({ read, from }) => ({
|
|
44
|
+
product: read(from(productPool, productId)),
|
|
45
|
+
reviews: read(from(reviewPool, productId)),
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
return { product, reviews, analytics };
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**If you don't need to pass value to another hook, use single useSelector.**
|
|
53
|
+
|
|
54
|
+
### Basic
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
const count = useSelector(count$); // Shorthand
|
|
58
|
+
const doubled = useSelector(({ read }) => read(count$) * 2); // Computed
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### With Pool
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
const user = useSelector(({ read, from }) => {
|
|
65
|
+
const user$ = from(userPool, userId);
|
|
66
|
+
return read(user$);
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### All Context Methods
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
const [user, posts] = useSelector(({ all }) => all([user$, posts$]));
|
|
74
|
+
const userState = useSelector(({ state }) => state(user$));
|
|
75
|
+
const result = useSelector(({ read, safe }) => {
|
|
76
|
+
const [err, data] = safe(() => JSON.parse(read(rawJson$)));
|
|
77
|
+
return err ? { error: err.message } : { data };
|
|
78
|
+
});
|
|
79
|
+
const canEdit = useSelector(({ and }) => and([isLoggedIn$, hasEditRole$]));
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Custom Equality
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
const userName = useSelector(
|
|
86
|
+
({ read }) => read(user$)?.name,
|
|
87
|
+
(prev, next) => prev === next
|
|
88
|
+
);
|
|
89
|
+
const data = useSelector(({ read }) => read(data$), "deep");
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## rx() — Inline Reactive
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
function Stats() {
|
|
96
|
+
return (
|
|
97
|
+
<footer>
|
|
98
|
+
{rx(({ read }) => {
|
|
99
|
+
const { total, completed } = read(stats$);
|
|
100
|
+
return <span>{completed} of {total}</span>;
|
|
101
|
+
})}
|
|
102
|
+
</footer>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### With Loading/Error
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
{rx(({ read }) => <UserCard user={read(user$)} />, {
|
|
111
|
+
loading: <Skeleton />,
|
|
112
|
+
error: (err) => <ErrorMessage error={err} />,
|
|
113
|
+
})}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### With deps
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
{rx(({ read }) => {
|
|
120
|
+
const user = read(user$);
|
|
121
|
+
return <ExpensiveComponent user={user} filter={filter} />;
|
|
122
|
+
}, { deps: [filter] })}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## useAction
|
|
126
|
+
|
|
127
|
+
Async ops with loading/error state.
|
|
128
|
+
|
|
129
|
+
### Basic
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
const save = useAction(async ({ signal }) => {
|
|
133
|
+
await saveData(data$.get(), { signal });
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<button onClick={save} disabled={save.status === "loading"}>
|
|
138
|
+
{save.status === "loading" ? "Saving..." : "Save"}
|
|
139
|
+
</button>
|
|
140
|
+
);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### API
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
const action = useAction(async ({ signal }) => fetchData({ signal }));
|
|
147
|
+
|
|
148
|
+
action(); // Call, returns AbortablePromise
|
|
149
|
+
await action();
|
|
150
|
+
action.abort(); // Abort current
|
|
151
|
+
|
|
152
|
+
action.status; // "idle" | "loading" | "success" | "error"
|
|
153
|
+
action.result; // TResult | undefined
|
|
154
|
+
action.error; // unknown
|
|
155
|
+
action.reset(); // Reset to idle
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Options
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
interface UseActionOptions {
|
|
162
|
+
lazy?: boolean; // true = manual call, false = auto on mount/deps
|
|
163
|
+
exclusive?: boolean; // true = abort previous, false = concurrent
|
|
164
|
+
deps?: unknown[]; // Re-execute when changed (lazy: false)
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Auto-execute (lazy: false)
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
const fetchUser = useAction(
|
|
172
|
+
async ({ signal }) => (await fetch(`/api/users/${userId}`, { signal })).json(),
|
|
173
|
+
{ lazy: false, deps: [userId] }
|
|
174
|
+
);
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Atom Deps (IMPORTANT)
|
|
178
|
+
|
|
179
|
+
**Pass atoms to `deps`, use `.get()` inside:**
|
|
180
|
+
|
|
181
|
+
```tsx
|
|
182
|
+
// ✅ REQUIRED
|
|
183
|
+
const loadData = useAction(
|
|
184
|
+
async ({ signal }) => {
|
|
185
|
+
const filter = filterAtom$.get();
|
|
186
|
+
const config = await configAtom$.get();
|
|
187
|
+
return fetchData(filter, config, { signal });
|
|
188
|
+
},
|
|
189
|
+
{ deps: [filterAtom$, configAtom$], lazy: false }
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// ❌ FORBIDDEN
|
|
193
|
+
const { filter, config } = useSelector(({ read }) => ({
|
|
194
|
+
filter: read(filterAtom$), // Suspends BEFORE useAction
|
|
195
|
+
config: read(configAtom$),
|
|
196
|
+
}));
|
|
197
|
+
const loadData = useAction(async () => fetchData(filter, config), {
|
|
198
|
+
deps: [filter, config], lazy: false,
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Error Handling
|
|
203
|
+
|
|
204
|
+
```tsx
|
|
205
|
+
const submit = useAction(async ({ signal }) => {
|
|
206
|
+
const res = await fetch("/api/submit", { method: "POST", signal });
|
|
207
|
+
if (!res.ok) throw new Error("Failed");
|
|
208
|
+
return res.json();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
{submit.status === "error" && <div className="error">{submit.error.message}</div>}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Form Pattern
|
|
215
|
+
|
|
216
|
+
```tsx
|
|
217
|
+
function ContactForm() {
|
|
218
|
+
const [formData, setFormData] = useState({ name: "", email: "" });
|
|
219
|
+
|
|
220
|
+
const submit = useAction(async ({ signal }) => {
|
|
221
|
+
if (!formData.name) throw new Error("Name required");
|
|
222
|
+
const res = await fetch("/api/contact", {
|
|
223
|
+
method: "POST",
|
|
224
|
+
body: JSON.stringify(formData),
|
|
225
|
+
signal,
|
|
226
|
+
});
|
|
227
|
+
if (!res.ok) throw new Error("Failed");
|
|
228
|
+
return res.json();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
<form onSubmit={(e) => { e.preventDefault(); submit(); }}>
|
|
233
|
+
<input value={formData.name} onChange={...} />
|
|
234
|
+
<button disabled={submit.status === "loading"}>
|
|
235
|
+
{submit.status === "loading" ? "Submitting..." : "Submit"}
|
|
236
|
+
</button>
|
|
237
|
+
{submit.status === "error" && <p className="error">{submit.error.message}</p>}
|
|
238
|
+
</form>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## useStable (CRITICAL)
|
|
244
|
+
|
|
245
|
+
**MUST use instead of React's useCallback/useMemo. NEVER use useCallback/useMemo.**
|
|
246
|
+
|
|
247
|
+
### Why
|
|
248
|
+
|
|
249
|
+
Inline objects/callbacks create new refs every render:
|
|
250
|
+
|
|
251
|
+
```tsx
|
|
252
|
+
// ❌ Problem: new refs every render
|
|
253
|
+
function Parent() {
|
|
254
|
+
const config = { theme: "dark" }; // New object!
|
|
255
|
+
const onClick = () => doSomething(); // New function!
|
|
256
|
+
return <Child config={config} onClick={onClick} />;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ✅ Solution
|
|
260
|
+
function Parent() {
|
|
261
|
+
const stable = useStable({
|
|
262
|
+
config: { theme: "dark" },
|
|
263
|
+
onClick: () => doSomething(),
|
|
264
|
+
});
|
|
265
|
+
return <Child config={stable.config} onClick={stable.onClick} />;
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### How It Works
|
|
270
|
+
|
|
271
|
+
| Type | Equality | Behavior |
|
|
272
|
+
| ---------- | ----------- | -------------------------------- |
|
|
273
|
+
| Functions | N/A | Stable ref, calls latest impl |
|
|
274
|
+
| Arrays | shallow | Stable if items equal |
|
|
275
|
+
| Dates | timestamp | Stable if same time |
|
|
276
|
+
| Objects | shallow | Stable if keys have equal values |
|
|
277
|
+
| Primitives | strict | Stable if same value |
|
|
278
|
+
|
|
279
|
+
### Basic
|
|
280
|
+
|
|
281
|
+
```tsx
|
|
282
|
+
const stable = useStable({
|
|
283
|
+
onSubmit: () => auth.register(username),
|
|
284
|
+
onLogin: () => auth.login(),
|
|
285
|
+
config: { timeout: 5000, retries: 3 },
|
|
286
|
+
columns: [{ key: "name", label: "Name" }],
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
stable.onSubmit();
|
|
290
|
+
<Table columns={stable.columns} />
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Custom Equality
|
|
294
|
+
|
|
295
|
+
```tsx
|
|
296
|
+
const stable = useStable(
|
|
297
|
+
{ user: { id: 1, profile: { name: "John" } } },
|
|
298
|
+
{ user: "deep" }
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
const stable2 = useStable(
|
|
302
|
+
{ user: { id: 1, updatedAt: new Date() } },
|
|
303
|
+
{ user: (a, b) => a?.id === b?.id }
|
|
304
|
+
);
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Logic Hook Pattern
|
|
308
|
+
|
|
309
|
+
```tsx
|
|
310
|
+
export function useAuthPageLogic() {
|
|
311
|
+
const auth = authStore();
|
|
312
|
+
const [view, setView] = useState("checking");
|
|
313
|
+
const [username, setUsername] = useState("");
|
|
314
|
+
|
|
315
|
+
const stable = useStable({
|
|
316
|
+
onRegister: async () => username.trim() && auth.register(username.trim()),
|
|
317
|
+
onLogin: async () => auth.login(),
|
|
318
|
+
onSwitchToRegister: () => { auth.clearError(); setView("register"); },
|
|
319
|
+
formOptions: { validateOnBlur: true },
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
return { view, username, setUsername, ...stable };
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### useStable vs useCallback
|
|
327
|
+
|
|
328
|
+
| Use Case | Use |
|
|
329
|
+
| ---------------------- | ------------------------- |
|
|
330
|
+
| Callbacks/handlers | `useStable` (ALWAYS) |
|
|
331
|
+
| Config objects | `useStable` (REQUIRED) |
|
|
332
|
+
| Arrays as props | `useStable` (REQUIRED) |
|
|
333
|
+
| Expensive computations | `useMemo` |
|
|
334
|
+
|
|
335
|
+
```tsx
|
|
336
|
+
// ❌ FORBIDDEN
|
|
337
|
+
const handleSubmit = useCallback(() => auth.register(username), [auth, username]);
|
|
338
|
+
const config = useMemo(() => ({ timeout: 5000 }), []);
|
|
339
|
+
|
|
340
|
+
// ✅ REQUIRED
|
|
341
|
+
const stable = useStable({
|
|
342
|
+
handleSubmit: () => auth.register(username),
|
|
343
|
+
config: { timeout: 5000 },
|
|
344
|
+
});
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
## Suspense (REQUIRED)
|
|
348
|
+
|
|
349
|
+
**MUST** wrap async atoms with `Suspense` and `ErrorBoundary`:
|
|
350
|
+
|
|
351
|
+
```tsx
|
|
352
|
+
function App() {
|
|
353
|
+
return (
|
|
354
|
+
<ErrorBoundary fallback={<ErrorPage />}>
|
|
355
|
+
<Suspense fallback={<Loading />}>
|
|
356
|
+
<Dashboard />
|
|
357
|
+
</Suspense>
|
|
358
|
+
</ErrorBoundary>
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function Dashboard() {
|
|
363
|
+
const user = useSelector(user$); // Suspends when loading
|
|
364
|
+
return <h1>Welcome, {user.name}</h1>;
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### Nested Boundaries
|
|
369
|
+
|
|
370
|
+
```tsx
|
|
371
|
+
function ArticlePage() {
|
|
372
|
+
return (
|
|
373
|
+
<Suspense fallback={<PageSkeleton />}>
|
|
374
|
+
<ArticleHeader />
|
|
375
|
+
<Suspense fallback={<CommentsSkeleton />}>
|
|
376
|
+
<ArticleComments />
|
|
377
|
+
</Suspense>
|
|
378
|
+
</Suspense>
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### Non-Suspense with state()
|
|
384
|
+
|
|
385
|
+
```tsx
|
|
386
|
+
function UserCard() {
|
|
387
|
+
const userState = useSelector(({ state }) => state(user$));
|
|
388
|
+
|
|
389
|
+
if (userState.status === "loading") return <Skeleton />;
|
|
390
|
+
if (userState.status === "error") return <Error error={userState.error} />;
|
|
391
|
+
return <div>{userState.value.name}</div>;
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
## Comparison
|
|
396
|
+
|
|
397
|
+
| Use Case | atomirx | Jotai |
|
|
398
|
+
| -------------- | ---------------------------------------- | --------------------------- |
|
|
399
|
+
| Single atom | `useSelector(atom$)` | `useAtomValue(atom)` |
|
|
400
|
+
| Derived | `useSelector(({ read }) => ...)` | `useAtomValue(derivedAtom)` |
|
|
401
|
+
| Multiple atoms | `useSelector(({ all }) => all([a$,b$]))` | Multiple hooks |
|
|
402
|
+
| Loadable | `useSelector(({ state }) => state(a$))` | `useAtomValue(loadable(a))` |
|
|
403
|
+
|
|
404
|
+
### Advantages
|
|
405
|
+
|
|
406
|
+
1. Single unified hook
|
|
407
|
+
2. Composable selectors
|
|
408
|
+
3. Flexible async modes
|
|
409
|
+
4. Built-in utilities: `all`, `any`, `race`, `settled`, `safe`, `state`, `and`, `or`
|
|
410
|
+
5. Type-safe
|
|
411
|
+
6. useStable — no dependency array footguns
|