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.
Files changed (138) hide show
  1. package/README.md +198 -2234
  2. package/bin/cli.js +90 -0
  3. package/dist/core/derived.d.ts +2 -2
  4. package/dist/core/effect.d.ts +3 -2
  5. package/dist/core/onCreateHook.d.ts +15 -2
  6. package/dist/core/onErrorHook.d.ts +4 -1
  7. package/dist/core/pool.d.ts +78 -0
  8. package/dist/core/pool.test.d.ts +1 -0
  9. package/dist/core/select-boolean.test.d.ts +1 -0
  10. package/dist/core/select-pool.test.d.ts +1 -0
  11. package/dist/core/select.d.ts +278 -86
  12. package/dist/core/types.d.ts +233 -1
  13. package/dist/core/withAbort.d.ts +95 -0
  14. package/dist/core/withReady.d.ts +3 -3
  15. package/dist/devtools/constants.d.ts +41 -0
  16. package/dist/devtools/index.cjs +1 -0
  17. package/dist/devtools/index.d.ts +29 -0
  18. package/dist/devtools/index.js +429 -0
  19. package/dist/devtools/registry.d.ts +98 -0
  20. package/dist/devtools/registry.test.d.ts +1 -0
  21. package/dist/devtools/setup.d.ts +61 -0
  22. package/dist/devtools/types.d.ts +311 -0
  23. package/dist/index-BZEnfIcB.cjs +1 -0
  24. package/dist/index-BbPZhsDl.js +1653 -0
  25. package/dist/index.cjs +1 -1
  26. package/dist/index.d.ts +4 -3
  27. package/dist/index.js +18 -14
  28. package/dist/onDispatchHook-C8yLzr-o.cjs +1 -0
  29. package/dist/onDispatchHook-SKbiIUaJ.js +5 -0
  30. package/dist/onErrorHook-BGGy3tqK.js +38 -0
  31. package/dist/onErrorHook-DHBASmYw.cjs +1 -0
  32. package/dist/react/index.cjs +1 -30
  33. package/dist/react/index.js +206 -791
  34. package/dist/react/onDispatchHook.d.ts +106 -0
  35. package/dist/react/useAction.d.ts +4 -1
  36. package/dist/react-devtools/DevToolsPanel.d.ts +93 -0
  37. package/dist/react-devtools/EntityDetails.d.ts +10 -0
  38. package/dist/react-devtools/EntityList.d.ts +15 -0
  39. package/dist/react-devtools/LogList.d.ts +12 -0
  40. package/dist/react-devtools/hooks.d.ts +50 -0
  41. package/dist/react-devtools/index.cjs +1 -0
  42. package/dist/react-devtools/index.d.ts +31 -0
  43. package/dist/react-devtools/index.js +1589 -0
  44. package/dist/react-devtools/styles.d.ts +148 -0
  45. package/package.json +26 -2
  46. package/skills/atomirx/SKILL.md +456 -0
  47. package/skills/atomirx/references/async-patterns.md +188 -0
  48. package/skills/atomirx/references/atom-patterns.md +238 -0
  49. package/skills/atomirx/references/deferred-loading.md +191 -0
  50. package/skills/atomirx/references/derived-patterns.md +428 -0
  51. package/skills/atomirx/references/effect-patterns.md +426 -0
  52. package/skills/atomirx/references/error-handling.md +140 -0
  53. package/skills/atomirx/references/hooks.md +322 -0
  54. package/skills/atomirx/references/pool-patterns.md +229 -0
  55. package/skills/atomirx/references/react-integration.md +411 -0
  56. package/skills/atomirx/references/rules.md +407 -0
  57. package/skills/atomirx/references/select-context.md +309 -0
  58. package/skills/atomirx/references/service-template.md +172 -0
  59. package/skills/atomirx/references/store-template.md +205 -0
  60. package/skills/atomirx/references/testing-patterns.md +431 -0
  61. package/coverage/base.css +0 -224
  62. package/coverage/block-navigation.js +0 -87
  63. package/coverage/clover.xml +0 -1440
  64. package/coverage/coverage-final.json +0 -14
  65. package/coverage/favicon.png +0 -0
  66. package/coverage/index.html +0 -131
  67. package/coverage/prettify.css +0 -1
  68. package/coverage/prettify.js +0 -2
  69. package/coverage/sort-arrow-sprite.png +0 -0
  70. package/coverage/sorter.js +0 -210
  71. package/coverage/src/core/atom.ts.html +0 -889
  72. package/coverage/src/core/batch.ts.html +0 -223
  73. package/coverage/src/core/define.ts.html +0 -805
  74. package/coverage/src/core/emitter.ts.html +0 -919
  75. package/coverage/src/core/equality.ts.html +0 -631
  76. package/coverage/src/core/hook.ts.html +0 -460
  77. package/coverage/src/core/index.html +0 -281
  78. package/coverage/src/core/isAtom.ts.html +0 -100
  79. package/coverage/src/core/isPromiseLike.ts.html +0 -133
  80. package/coverage/src/core/onCreateHook.ts.html +0 -138
  81. package/coverage/src/core/scheduleNotifyHook.ts.html +0 -94
  82. package/coverage/src/core/types.ts.html +0 -523
  83. package/coverage/src/core/withUse.ts.html +0 -253
  84. package/coverage/src/index.html +0 -116
  85. package/coverage/src/index.ts.html +0 -106
  86. package/dist/index-CBVj1kSj.js +0 -1350
  87. package/dist/index-Cxk9v0um.cjs +0 -1
  88. package/scripts/publish.js +0 -198
  89. package/src/core/atom.test.ts +0 -633
  90. package/src/core/atom.ts +0 -311
  91. package/src/core/atomState.test.ts +0 -342
  92. package/src/core/atomState.ts +0 -256
  93. package/src/core/batch.test.ts +0 -257
  94. package/src/core/batch.ts +0 -172
  95. package/src/core/define.test.ts +0 -343
  96. package/src/core/define.ts +0 -243
  97. package/src/core/derived.test.ts +0 -1215
  98. package/src/core/derived.ts +0 -450
  99. package/src/core/effect.test.ts +0 -802
  100. package/src/core/effect.ts +0 -188
  101. package/src/core/emitter.test.ts +0 -364
  102. package/src/core/emitter.ts +0 -392
  103. package/src/core/equality.test.ts +0 -392
  104. package/src/core/equality.ts +0 -182
  105. package/src/core/getAtomState.ts +0 -69
  106. package/src/core/hook.test.ts +0 -227
  107. package/src/core/hook.ts +0 -177
  108. package/src/core/isAtom.ts +0 -27
  109. package/src/core/isPromiseLike.test.ts +0 -72
  110. package/src/core/isPromiseLike.ts +0 -16
  111. package/src/core/onCreateHook.ts +0 -107
  112. package/src/core/onErrorHook.test.ts +0 -350
  113. package/src/core/onErrorHook.ts +0 -52
  114. package/src/core/promiseCache.test.ts +0 -241
  115. package/src/core/promiseCache.ts +0 -284
  116. package/src/core/scheduleNotifyHook.ts +0 -53
  117. package/src/core/select.ts +0 -729
  118. package/src/core/selector.test.ts +0 -799
  119. package/src/core/types.ts +0 -389
  120. package/src/core/withReady.test.ts +0 -534
  121. package/src/core/withReady.ts +0 -191
  122. package/src/core/withUse.test.ts +0 -249
  123. package/src/core/withUse.ts +0 -56
  124. package/src/index.test.ts +0 -80
  125. package/src/index.ts +0 -65
  126. package/src/react/index.ts +0 -21
  127. package/src/react/rx.test.tsx +0 -571
  128. package/src/react/rx.tsx +0 -531
  129. package/src/react/strictModeTest.tsx +0 -71
  130. package/src/react/useAction.test.ts +0 -987
  131. package/src/react/useAction.ts +0 -607
  132. package/src/react/useSelector.test.ts +0 -182
  133. package/src/react/useSelector.ts +0 -292
  134. package/src/react/useStable.test.ts +0 -553
  135. package/src/react/useStable.ts +0 -288
  136. package/tsconfig.json +0 -9
  137. package/v2.md +0 -725
  138. 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
- ];