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
package/src/react/rx.tsx
DELETED
|
@@ -1,531 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Component,
|
|
3
|
-
memo,
|
|
4
|
-
ReactElement,
|
|
5
|
-
ReactNode,
|
|
6
|
-
Suspense,
|
|
7
|
-
ErrorInfo,
|
|
8
|
-
useCallback,
|
|
9
|
-
useRef,
|
|
10
|
-
} from "react";
|
|
11
|
-
import { Atom, Equality } from "../core/types";
|
|
12
|
-
import { useSelector } from "./useSelector";
|
|
13
|
-
import { shallowEqual } from "../core/equality";
|
|
14
|
-
import { isAtom } from "../core/isAtom";
|
|
15
|
-
import { ReactiveSelector, SelectContext } from "../core/select";
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Options for rx() with inline loading/error handling and memoization control.
|
|
19
|
-
*/
|
|
20
|
-
export interface RxOptions<T> {
|
|
21
|
-
/** Equality function for value comparison */
|
|
22
|
-
equals?: Equality<T>;
|
|
23
|
-
/** Render function for loading state */
|
|
24
|
-
loading?: () => ReactNode;
|
|
25
|
-
/** Render function for error state */
|
|
26
|
-
error?: (props: { error: unknown }) => ReactNode;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Dependencies array for selector memoization.
|
|
30
|
-
*
|
|
31
|
-
* Controls when the selector callback is recreated:
|
|
32
|
-
* - **Atom shorthand** (`rx(atom$)`): Always memoized by atom reference (deps ignored)
|
|
33
|
-
* - **Function selector without deps**: No memoization (recreated every render)
|
|
34
|
-
* - **Function selector with `deps: []`**: Stable forever (never recreated)
|
|
35
|
-
* - **Function selector with `deps: [a, b]`**: Recreated when deps change
|
|
36
|
-
*
|
|
37
|
-
* @example
|
|
38
|
-
* ```tsx
|
|
39
|
-
* // No memoization (default for functions) - selector recreated every render
|
|
40
|
-
* rx(({ read }) => read(count$) * 2)
|
|
41
|
-
*
|
|
42
|
-
* // Stable selector - never recreated
|
|
43
|
-
* rx(({ read }) => read(count$) * 2, { deps: [] })
|
|
44
|
-
*
|
|
45
|
-
* // Recreate when multiplier changes
|
|
46
|
-
* rx(({ read }) => read(count$) * multiplier, { deps: [multiplier] })
|
|
47
|
-
* ```
|
|
48
|
-
*/
|
|
49
|
-
deps?: unknown[];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Reactive inline component that renders atom values directly in JSX.
|
|
54
|
-
*
|
|
55
|
-
* `rx` is a convenience wrapper around `useSelector` that returns a memoized
|
|
56
|
-
* React component instead of a value. This enables fine-grained reactivity
|
|
57
|
-
* without creating separate components for each reactive value.
|
|
58
|
-
*
|
|
59
|
-
* ## IMPORTANT: Selector Must Return Synchronous Value
|
|
60
|
-
*
|
|
61
|
-
* **The selector function MUST NOT be async or return a Promise.**
|
|
62
|
-
*
|
|
63
|
-
* ```tsx
|
|
64
|
-
* // ❌ WRONG - Don't use async function
|
|
65
|
-
* rx(async ({ read }) => {
|
|
66
|
-
* const data = await fetch('/api');
|
|
67
|
-
* return data.name;
|
|
68
|
-
* });
|
|
69
|
-
*
|
|
70
|
-
* // ❌ WRONG - Don't return a Promise
|
|
71
|
-
* rx(({ read }) => fetch('/api').then(r => r.json()));
|
|
72
|
-
*
|
|
73
|
-
* // ✅ CORRECT - Create async atom and read with read()
|
|
74
|
-
* const data$ = atom(fetch('/api').then(r => r.json()));
|
|
75
|
-
* rx(({ read }) => read(data$).name); // Suspends until resolved
|
|
76
|
-
* ```
|
|
77
|
-
*
|
|
78
|
-
* ## IMPORTANT: Do NOT Use try/catch - Use safe() Instead
|
|
79
|
-
*
|
|
80
|
-
* **Never wrap `read()` calls in try/catch blocks.** The `read()` function throws
|
|
81
|
-
* Promises when atoms are loading (Suspense pattern). A try/catch will catch
|
|
82
|
-
* these Promises and break the Suspense mechanism.
|
|
83
|
-
*
|
|
84
|
-
* ```tsx
|
|
85
|
-
* // ❌ WRONG - Catches Suspense Promise, breaks loading state
|
|
86
|
-
* rx(({ read }) => {
|
|
87
|
-
* try {
|
|
88
|
-
* return <span>{read(user$).name}</span>;
|
|
89
|
-
* } catch (e) {
|
|
90
|
-
* return <span>Error</span>; // Catches BOTH errors AND loading promises!
|
|
91
|
-
* }
|
|
92
|
-
* });
|
|
93
|
-
*
|
|
94
|
-
* // ✅ CORRECT - Use safe() to catch errors but preserve Suspense
|
|
95
|
-
* rx(({ read, safe }) => {
|
|
96
|
-
* const [err, user] = safe(() => read(user$));
|
|
97
|
-
* if (err) return <span>Error: {err.message}</span>;
|
|
98
|
-
* return <span>{user.name}</span>;
|
|
99
|
-
* });
|
|
100
|
-
* ```
|
|
101
|
-
*
|
|
102
|
-
* The `safe()` utility:
|
|
103
|
-
* - **Catches errors** and returns `[error, undefined]`
|
|
104
|
-
* - **Re-throws Promises** to preserve Suspense behavior
|
|
105
|
-
* - Returns `[undefined, result]` on success
|
|
106
|
-
*
|
|
107
|
-
* ## Why Use `rx`?
|
|
108
|
-
*
|
|
109
|
-
* Without `rx`, you need a separate component to subscribe to an atom:
|
|
110
|
-
* ```tsx
|
|
111
|
-
* function PostsList() {
|
|
112
|
-
* const posts = useSelector(postsAtom);
|
|
113
|
-
* return posts.map((post) => <Post post={post} />);
|
|
114
|
-
* }
|
|
115
|
-
*
|
|
116
|
-
* function Page() {
|
|
117
|
-
* return (
|
|
118
|
-
* <Suspense fallback={<Loading />}>
|
|
119
|
-
* <PostsList />
|
|
120
|
-
* </Suspense>
|
|
121
|
-
* );
|
|
122
|
-
* }
|
|
123
|
-
* ```
|
|
124
|
-
*
|
|
125
|
-
* With `rx`, you can subscribe inline:
|
|
126
|
-
* ```tsx
|
|
127
|
-
* function Page() {
|
|
128
|
-
* return (
|
|
129
|
-
* <Suspense fallback={<Loading />}>
|
|
130
|
-
* {rx(({ read }) =>
|
|
131
|
-
* read(postsAtom).map((post) => <Post post={post} />)
|
|
132
|
-
* )}
|
|
133
|
-
* </Suspense>
|
|
134
|
-
* );
|
|
135
|
-
* }
|
|
136
|
-
* ```
|
|
137
|
-
*
|
|
138
|
-
* ## Key Benefits
|
|
139
|
-
*
|
|
140
|
-
* 1. **Fine-grained updates**: Only the `rx` component re-renders when the atom changes,
|
|
141
|
-
* not the parent component
|
|
142
|
-
* 2. **Less boilerplate**: No need to create single-purpose wrapper components
|
|
143
|
-
* 3. **Colocation**: Keep reactive logic inline where it's used
|
|
144
|
-
* 4. **Memoized**: Uses `React.memo` to prevent unnecessary re-renders
|
|
145
|
-
* 5. **Type-safe**: Full TypeScript support with proper type inference
|
|
146
|
-
*
|
|
147
|
-
* ## Async Atoms (Suspense-Style API)
|
|
148
|
-
*
|
|
149
|
-
* `rx` inherits the Suspense-style API from `useSelector`:
|
|
150
|
-
* - **Loading state**: The getter throws a Promise (triggers Suspense)
|
|
151
|
-
* - **Error state**: The getter throws the error (triggers ErrorBoundary)
|
|
152
|
-
* - **Resolved state**: The getter returns the value
|
|
153
|
-
*
|
|
154
|
-
* For async atoms, you MUST wrap with `<Suspense>` and `<ErrorBoundary>`:
|
|
155
|
-
* ```tsx
|
|
156
|
-
* function App() {
|
|
157
|
-
* return (
|
|
158
|
-
* <ErrorBoundary fallback={<div>Error!</div>}>
|
|
159
|
-
* <Suspense fallback={<div>Loading...</div>}>
|
|
160
|
-
* {rx(({ read }) => read(userAtom).name)}
|
|
161
|
-
* </Suspense>
|
|
162
|
-
* </ErrorBoundary>
|
|
163
|
-
* );
|
|
164
|
-
* }
|
|
165
|
-
* ```
|
|
166
|
-
*
|
|
167
|
-
* Or catch errors in the selector to handle loading/error inline:
|
|
168
|
-
* ```tsx
|
|
169
|
-
* {rx(({ read }) => {
|
|
170
|
-
* try {
|
|
171
|
-
* return read(userAtom).name;
|
|
172
|
-
* } catch {
|
|
173
|
-
* return "Loading...";
|
|
174
|
-
* }
|
|
175
|
-
* })}
|
|
176
|
-
* ```
|
|
177
|
-
*
|
|
178
|
-
* @template T - The type of the selected/derived value
|
|
179
|
-
* @param selector - Context-based selector function with `{ read, all, any, race, settled }`.
|
|
180
|
-
* Must return sync value, not a Promise.
|
|
181
|
-
* @param equals - Equality function or shorthand ("strict", "shallow", "deep").
|
|
182
|
-
* Defaults to "shallow".
|
|
183
|
-
* @returns A React element that renders the selected value
|
|
184
|
-
* @throws Error if selector returns a Promise or PromiseLike
|
|
185
|
-
*
|
|
186
|
-
* ## IMPORTANT: Atom Value Must Be ReactNode
|
|
187
|
-
*
|
|
188
|
-
* When using the shorthand `rx(atom)`, the atom's value must be a valid `ReactNode`
|
|
189
|
-
* (string, number, boolean, null, undefined, or React element). Objects and arrays
|
|
190
|
-
* are NOT valid ReactNode values and will cause React to throw an error.
|
|
191
|
-
*
|
|
192
|
-
* ```tsx
|
|
193
|
-
* // ✅ CORRECT - Atom contains ReactNode (number)
|
|
194
|
-
* const count$ = atom(5);
|
|
195
|
-
* rx(count$);
|
|
196
|
-
*
|
|
197
|
-
* // ✅ CORRECT - Atom contains ReactNode (string)
|
|
198
|
-
* const name$ = atom("John");
|
|
199
|
-
* rx(name$);
|
|
200
|
-
*
|
|
201
|
-
* // ❌ WRONG - Atom contains object (not ReactNode)
|
|
202
|
-
* const user$ = atom({ name: "John", age: 30 });
|
|
203
|
-
* rx(user$); // React error: "Objects are not valid as a React child"
|
|
204
|
-
*
|
|
205
|
-
* // ✅ CORRECT - Use selector to extract ReactNode from object
|
|
206
|
-
* rx(({ read }) => read(user$).name);
|
|
207
|
-
* rx(({ read }) => <UserCard user={read(user$)} />);
|
|
208
|
-
* ```
|
|
209
|
-
*
|
|
210
|
-
* @example Shorthand - render atom value directly
|
|
211
|
-
* ```tsx
|
|
212
|
-
* const count = atom(5);
|
|
213
|
-
*
|
|
214
|
-
* function Counter() {
|
|
215
|
-
* return <div>Count: {rx(count)}</div>;
|
|
216
|
-
* }
|
|
217
|
-
* ```
|
|
218
|
-
*
|
|
219
|
-
* @example Context selector - derive a value
|
|
220
|
-
* ```tsx
|
|
221
|
-
* const count = atom(5);
|
|
222
|
-
*
|
|
223
|
-
* function DoubledCounter() {
|
|
224
|
-
* return <div>Doubled: {rx(({ read }) => read(count) * 2)}</div>;
|
|
225
|
-
* }
|
|
226
|
-
* ```
|
|
227
|
-
*
|
|
228
|
-
* @example Multiple atoms
|
|
229
|
-
* ```tsx
|
|
230
|
-
* const firstName = atom("John");
|
|
231
|
-
* const lastName = atom("Doe");
|
|
232
|
-
*
|
|
233
|
-
* function FullName() {
|
|
234
|
-
* return (
|
|
235
|
-
* <div>
|
|
236
|
-
* {rx(({ read }) => `${read(firstName)} ${read(lastName)}`)}
|
|
237
|
-
* </div>
|
|
238
|
-
* );
|
|
239
|
-
* }
|
|
240
|
-
* ```
|
|
241
|
-
*
|
|
242
|
-
* @example Fine-grained updates - parent doesn't re-render
|
|
243
|
-
* ```tsx
|
|
244
|
-
* const count = atom(0);
|
|
245
|
-
*
|
|
246
|
-
* function Parent() {
|
|
247
|
-
* console.log("Parent renders once");
|
|
248
|
-
* return (
|
|
249
|
-
* <div>
|
|
250
|
-
* {rx(count)} {/* Only this re-renders when count changes *\/}
|
|
251
|
-
* <button onClick={() => count.set((n) => n + 1)}>+</button>
|
|
252
|
-
* </div>
|
|
253
|
-
* );
|
|
254
|
-
* }
|
|
255
|
-
* ```
|
|
256
|
-
*
|
|
257
|
-
* @example Multiple subscriptions in one component
|
|
258
|
-
* ```tsx
|
|
259
|
-
* function Dashboard() {
|
|
260
|
-
* return (
|
|
261
|
-
* <div>
|
|
262
|
-
* <header>
|
|
263
|
-
* <Suspense fallback="...">{rx(({ read }) => read(userAtom).name)}</Suspense>
|
|
264
|
-
* </header>
|
|
265
|
-
* <main>
|
|
266
|
-
* <Suspense fallback="...">
|
|
267
|
-
* {rx(({ read }) => read(postsAtom).length)} posts
|
|
268
|
-
* </Suspense>
|
|
269
|
-
* <Suspense fallback="...">
|
|
270
|
-
* {rx(({ read }) => read(notificationsAtom).length)} notifications
|
|
271
|
-
* </Suspense>
|
|
272
|
-
* </main>
|
|
273
|
-
* </div>
|
|
274
|
-
* );
|
|
275
|
-
* }
|
|
276
|
-
* ```
|
|
277
|
-
*
|
|
278
|
-
* @example Conditional dependencies - only subscribes to accessed atoms
|
|
279
|
-
* ```tsx
|
|
280
|
-
* const showDetails = atom(false);
|
|
281
|
-
* const summary = atom("Brief info");
|
|
282
|
-
* const details = atom("Detailed info");
|
|
283
|
-
*
|
|
284
|
-
* function Info() {
|
|
285
|
-
* return (
|
|
286
|
-
* <div>
|
|
287
|
-
* {rx(({ read }) =>
|
|
288
|
-
* read(showDetails) ? read(details) : read(summary)
|
|
289
|
-
* )}
|
|
290
|
-
* </div>
|
|
291
|
-
* );
|
|
292
|
-
* }
|
|
293
|
-
* ```
|
|
294
|
-
*
|
|
295
|
-
* @example With custom equality
|
|
296
|
-
* ```tsx
|
|
297
|
-
* const user = atom({ id: 1, name: "John" });
|
|
298
|
-
*
|
|
299
|
-
* function UserName() {
|
|
300
|
-
* return (
|
|
301
|
-
* <div>
|
|
302
|
-
* {rx(
|
|
303
|
-
* ({ read }) => read(user).name,
|
|
304
|
-
* (a, b) => a === b // Only re-render if name string changes
|
|
305
|
-
* )}
|
|
306
|
-
* </div>
|
|
307
|
-
* );
|
|
308
|
-
* }
|
|
309
|
-
* ```
|
|
310
|
-
*
|
|
311
|
-
* @example Combining multiple async atoms with async utilities
|
|
312
|
-
* ```tsx
|
|
313
|
-
* const userAtom = atom(fetchUser());
|
|
314
|
-
* const postsAtom = atom(fetchPosts());
|
|
315
|
-
*
|
|
316
|
-
* function Dashboard() {
|
|
317
|
-
* return (
|
|
318
|
-
* <Suspense fallback={<Loading />}>
|
|
319
|
-
* {rx(({ all }) => {
|
|
320
|
-
* // Use all() to wait for multiple atoms
|
|
321
|
-
* const [user, posts] = all([user$, posts$]);
|
|
322
|
-
* return <DashboardContent user={user} posts={posts} />;
|
|
323
|
-
* })}
|
|
324
|
-
* </Suspense>
|
|
325
|
-
* );
|
|
326
|
-
* }
|
|
327
|
-
* ```
|
|
328
|
-
*
|
|
329
|
-
* @example Using settled for partial failures
|
|
330
|
-
* ```tsx
|
|
331
|
-
* const userAtom = atom(fetchUser());
|
|
332
|
-
* const postsAtom = atom(fetchPosts());
|
|
333
|
-
*
|
|
334
|
-
* function Dashboard() {
|
|
335
|
-
* return (
|
|
336
|
-
* <Suspense fallback={<Loading />}>
|
|
337
|
-
* {rx(({ settled }) => {
|
|
338
|
-
* const [userResult, postsResult] = settled([userAtom, postsAtom]);
|
|
339
|
-
* return (
|
|
340
|
-
* <DashboardContent
|
|
341
|
-
* user={userResult.status === 'resolved' ? userResult.value : null}
|
|
342
|
-
* posts={postsResult.status === 'resolved' ? postsResult.value : []}
|
|
343
|
-
* />
|
|
344
|
-
* );
|
|
345
|
-
* })}
|
|
346
|
-
* </Suspense>
|
|
347
|
-
* );
|
|
348
|
-
* }
|
|
349
|
-
* ```
|
|
350
|
-
*/
|
|
351
|
-
// Overload: Pass atom directly to get its value (shorthand)
|
|
352
|
-
export function rx<T extends ReactNode | PromiseLike<ReactNode>>(
|
|
353
|
-
atom: Atom<T>,
|
|
354
|
-
options?: Equality<T> | RxOptions<T>
|
|
355
|
-
): ReactElement;
|
|
356
|
-
|
|
357
|
-
// Overload: Context-based selector function
|
|
358
|
-
export function rx<T extends ReactNode | PromiseLike<ReactNode>>(
|
|
359
|
-
selector: ReactiveSelector<T>,
|
|
360
|
-
options?: Equality<T> | RxOptions<T>
|
|
361
|
-
): ReactElement;
|
|
362
|
-
|
|
363
|
-
export function rx<T>(
|
|
364
|
-
selectorOrAtom: ReactiveSelector<T> | Atom<T>,
|
|
365
|
-
options?: Equality<unknown> | RxOptions<unknown>
|
|
366
|
-
): ReactElement {
|
|
367
|
-
// Normalize options
|
|
368
|
-
const normalizedOptions: RxOptions<unknown> | undefined =
|
|
369
|
-
options === undefined
|
|
370
|
-
? undefined
|
|
371
|
-
: typeof options === "object" &&
|
|
372
|
-
options !== null &&
|
|
373
|
-
!Array.isArray(options) &&
|
|
374
|
-
("equals" in options || "loading" in options || "error" in options)
|
|
375
|
-
? (options as RxOptions<unknown>)
|
|
376
|
-
: { equals: options as Equality<unknown> };
|
|
377
|
-
|
|
378
|
-
return (
|
|
379
|
-
<Rx
|
|
380
|
-
selectorOrAtom={
|
|
381
|
-
selectorOrAtom as ReactiveSelector<unknown> | Atom<unknown>
|
|
382
|
-
}
|
|
383
|
-
options={normalizedOptions}
|
|
384
|
-
/>
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* Internal ErrorBoundary for rx with error handler.
|
|
390
|
-
*/
|
|
391
|
-
interface RxErrorBoundaryProps {
|
|
392
|
-
children: ReactNode;
|
|
393
|
-
onError?: (props: { error: unknown }) => ReactNode;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
interface RxErrorBoundaryState {
|
|
397
|
-
error: unknown | null;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
class RxErrorBoundary extends Component<
|
|
401
|
-
RxErrorBoundaryProps,
|
|
402
|
-
RxErrorBoundaryState
|
|
403
|
-
> {
|
|
404
|
-
state: RxErrorBoundaryState = { error: null };
|
|
405
|
-
|
|
406
|
-
static getDerivedStateFromError(error: unknown): RxErrorBoundaryState {
|
|
407
|
-
return { error };
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
411
|
-
componentDidCatch(_error: Error, _errorInfo: ErrorInfo) {
|
|
412
|
-
// Error already captured in state
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
render() {
|
|
416
|
-
if (this.state.error !== null && this.props.onError) {
|
|
417
|
-
return <>{this.props.onError({ error: this.state.error })}</>;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
if (this.state.error !== null) {
|
|
421
|
-
// No handler - re-throw to parent ErrorBoundary
|
|
422
|
-
throw this.state.error;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
return this.props.children;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
/**
|
|
430
|
-
* Internal component that renders the selector value.
|
|
431
|
-
*/
|
|
432
|
-
function RxInner(props: {
|
|
433
|
-
selector: ReactiveSelector<unknown>;
|
|
434
|
-
equals?: Equality<unknown>;
|
|
435
|
-
}) {
|
|
436
|
-
const selected = useSelector(props.selector, props.equals);
|
|
437
|
-
return <>{selected ?? null}</>;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
/**
|
|
441
|
-
* Wrapper component to defer loading() call until actually needed.
|
|
442
|
-
*/
|
|
443
|
-
function RxLoadingFallback(props: { render: () => ReactNode }) {
|
|
444
|
-
return <>{props.render()}</>;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
/**
|
|
448
|
-
* Optional Suspense wrapper - only wraps if fallback is provided.
|
|
449
|
-
*/
|
|
450
|
-
function RxSuspenseWrapper(props: {
|
|
451
|
-
fallback?: () => ReactNode;
|
|
452
|
-
children: ReactNode;
|
|
453
|
-
}) {
|
|
454
|
-
if (props.fallback) {
|
|
455
|
-
return (
|
|
456
|
-
<Suspense fallback={<RxLoadingFallback render={props.fallback} />}>
|
|
457
|
-
{props.children}
|
|
458
|
-
</Suspense>
|
|
459
|
-
);
|
|
460
|
-
}
|
|
461
|
-
return <>{props.children}</>;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* Optional ErrorBoundary wrapper - only wraps if onError is provided.
|
|
466
|
-
*/
|
|
467
|
-
function RxErrorWrapper(props: {
|
|
468
|
-
onError?: (props: { error: unknown }) => ReactNode;
|
|
469
|
-
children: ReactNode;
|
|
470
|
-
}) {
|
|
471
|
-
if (props.onError) {
|
|
472
|
-
return (
|
|
473
|
-
<RxErrorBoundary onError={props.onError}>
|
|
474
|
-
{props.children}
|
|
475
|
-
</RxErrorBoundary>
|
|
476
|
-
);
|
|
477
|
-
}
|
|
478
|
-
return <>{props.children}</>;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
/**
|
|
482
|
-
* Internal memoized component that handles the actual subscription and rendering.
|
|
483
|
-
*
|
|
484
|
-
* Memoized with React.memo to ensure:
|
|
485
|
-
* 1. Parent components don't cause unnecessary re-renders
|
|
486
|
-
* 2. Only atom changes trigger re-renders
|
|
487
|
-
* 3. Props comparison is shallow (selectorOrAtom, options references)
|
|
488
|
-
*
|
|
489
|
-
* Renders `selected ?? null` to handle null/undefined values gracefully in JSX.
|
|
490
|
-
*/
|
|
491
|
-
const Rx = memo(
|
|
492
|
-
function Rx(props: {
|
|
493
|
-
selectorOrAtom: ReactiveSelector<unknown> | Atom<unknown>;
|
|
494
|
-
options?: RxOptions<unknown>;
|
|
495
|
-
}) {
|
|
496
|
-
// Store latest selector/atom in ref to avoid stale closures
|
|
497
|
-
const selectorRef = useRef(props.selectorOrAtom);
|
|
498
|
-
selectorRef.current = props.selectorOrAtom;
|
|
499
|
-
|
|
500
|
-
// Compute memoization dependencies:
|
|
501
|
-
// - Atom: always include atom reference for stability
|
|
502
|
-
// - Function + no deps: new object each render (no memoization)
|
|
503
|
-
// - Function + deps: use provided deps for controlled memoization
|
|
504
|
-
const isAtomInput = isAtom(props.selectorOrAtom);
|
|
505
|
-
const userDeps = props.options?.deps;
|
|
506
|
-
const deps = isAtomInput
|
|
507
|
-
? [props.selectorOrAtom, ...(userDeps ?? [])] // Atom: stable + optional user deps
|
|
508
|
-
: (userDeps ?? [{}]); // Function: user deps or no memoization
|
|
509
|
-
|
|
510
|
-
// Memoized selector that reads from ref to always get latest value
|
|
511
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
512
|
-
const selector = useCallback(
|
|
513
|
-
(context: SelectContext) =>
|
|
514
|
-
isAtom(selectorRef.current)
|
|
515
|
-
? context.read(selectorRef.current as Atom<unknown>)
|
|
516
|
-
: (selectorRef.current as ReactiveSelector<unknown>)(context),
|
|
517
|
-
deps
|
|
518
|
-
);
|
|
519
|
-
|
|
520
|
-
return (
|
|
521
|
-
<RxErrorWrapper onError={props.options?.error}>
|
|
522
|
-
<RxSuspenseWrapper fallback={props.options?.loading}>
|
|
523
|
-
<RxInner selector={selector} equals={props.options?.equals} />
|
|
524
|
-
</RxSuspenseWrapper>
|
|
525
|
-
</RxErrorWrapper>
|
|
526
|
-
);
|
|
527
|
-
},
|
|
528
|
-
(prev, next) =>
|
|
529
|
-
shallowEqual(prev.selectorOrAtom, next.selectorOrAtom) &&
|
|
530
|
-
shallowEqual(prev.options, next.options)
|
|
531
|
-
);
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { PropsWithChildren, StrictMode } from "react";
|
|
3
|
-
import { render, renderHook, RenderHookOptions } from "@testing-library/react";
|
|
4
|
-
|
|
5
|
-
const DefaultWrapper = ({ children }: PropsWithChildren) => <>{children}</>;
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Composes two React wrapper components.
|
|
9
|
-
* OuterWrapper wraps InnerWrapper which wraps children.
|
|
10
|
-
*/
|
|
11
|
-
function composeWrappers(
|
|
12
|
-
OuterWrapper: React.ComponentType<{ children: React.ReactNode }>,
|
|
13
|
-
InnerWrapper?: React.ComponentType<{ children: React.ReactNode }>
|
|
14
|
-
): React.ComponentType<{ children: React.ReactNode }> {
|
|
15
|
-
if (!InnerWrapper) {
|
|
16
|
-
return OuterWrapper;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
return function ComposedWrapper({ children }: { children: React.ReactNode }) {
|
|
20
|
-
return (
|
|
21
|
-
<OuterWrapper>
|
|
22
|
-
<InnerWrapper>{children}</InnerWrapper>
|
|
23
|
-
</OuterWrapper>
|
|
24
|
-
);
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const StrictModeWrapper = ({ children }: PropsWithChildren) => (
|
|
29
|
-
<StrictMode>{children}</StrictMode>
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
export const wrappers: {
|
|
33
|
-
mode: "normal" | "strict";
|
|
34
|
-
Wrapper: React.FC<{ children: React.ReactNode }>;
|
|
35
|
-
render: (ui: React.ReactElement) => ReturnType<typeof render>;
|
|
36
|
-
renderHook: <TResult, TProps>(
|
|
37
|
-
render: (props: TProps) => TResult,
|
|
38
|
-
options?: RenderHookOptions<TProps>
|
|
39
|
-
) => ReturnType<typeof renderHook<TResult, TProps>>;
|
|
40
|
-
}[] = [
|
|
41
|
-
{
|
|
42
|
-
mode: "normal" as const,
|
|
43
|
-
Wrapper: DefaultWrapper,
|
|
44
|
-
render: (ui: React.ReactElement) => {
|
|
45
|
-
return render(ui, { wrapper: DefaultWrapper });
|
|
46
|
-
},
|
|
47
|
-
renderHook: <TResult, TProps>(
|
|
48
|
-
callback: (props: TProps) => TResult,
|
|
49
|
-
options?: RenderHookOptions<TProps>
|
|
50
|
-
) => {
|
|
51
|
-
return renderHook(callback, options);
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
mode: "strict" as const,
|
|
56
|
-
Wrapper: StrictModeWrapper,
|
|
57
|
-
render: (ui: React.ReactElement) => {
|
|
58
|
-
return render(ui, { wrapper: StrictModeWrapper });
|
|
59
|
-
},
|
|
60
|
-
renderHook: <TResult, TProps>(
|
|
61
|
-
callback: (props: TProps) => TResult,
|
|
62
|
-
options?: RenderHookOptions<TProps>
|
|
63
|
-
) => {
|
|
64
|
-
const composedWrapper = composeWrappers(
|
|
65
|
-
StrictModeWrapper,
|
|
66
|
-
options?.wrapper
|
|
67
|
-
);
|
|
68
|
-
return renderHook(callback, { ...options, wrapper: composedWrapper });
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
];
|