atomirx 0.0.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.
Files changed (121) hide show
  1. package/README.md +1666 -0
  2. package/coverage/base.css +224 -0
  3. package/coverage/block-navigation.js +87 -0
  4. package/coverage/clover.xml +1440 -0
  5. package/coverage/coverage-final.json +14 -0
  6. package/coverage/favicon.png +0 -0
  7. package/coverage/index.html +131 -0
  8. package/coverage/prettify.css +1 -0
  9. package/coverage/prettify.js +2 -0
  10. package/coverage/sort-arrow-sprite.png +0 -0
  11. package/coverage/sorter.js +210 -0
  12. package/coverage/src/core/atom.ts.html +889 -0
  13. package/coverage/src/core/batch.ts.html +223 -0
  14. package/coverage/src/core/define.ts.html +805 -0
  15. package/coverage/src/core/emitter.ts.html +919 -0
  16. package/coverage/src/core/equality.ts.html +631 -0
  17. package/coverage/src/core/hook.ts.html +460 -0
  18. package/coverage/src/core/index.html +281 -0
  19. package/coverage/src/core/isAtom.ts.html +100 -0
  20. package/coverage/src/core/isPromiseLike.ts.html +133 -0
  21. package/coverage/src/core/onCreateHook.ts.html +136 -0
  22. package/coverage/src/core/scheduleNotifyHook.ts.html +94 -0
  23. package/coverage/src/core/types.ts.html +523 -0
  24. package/coverage/src/core/withUse.ts.html +253 -0
  25. package/coverage/src/index.html +116 -0
  26. package/coverage/src/index.ts.html +106 -0
  27. package/dist/core/atom.d.ts +63 -0
  28. package/dist/core/atom.test.d.ts +1 -0
  29. package/dist/core/atomState.d.ts +104 -0
  30. package/dist/core/atomState.test.d.ts +1 -0
  31. package/dist/core/batch.d.ts +126 -0
  32. package/dist/core/batch.test.d.ts +1 -0
  33. package/dist/core/define.d.ts +173 -0
  34. package/dist/core/define.test.d.ts +1 -0
  35. package/dist/core/derived.d.ts +102 -0
  36. package/dist/core/derived.test.d.ts +1 -0
  37. package/dist/core/effect.d.ts +120 -0
  38. package/dist/core/effect.test.d.ts +1 -0
  39. package/dist/core/emitter.d.ts +237 -0
  40. package/dist/core/emitter.test.d.ts +1 -0
  41. package/dist/core/equality.d.ts +62 -0
  42. package/dist/core/equality.test.d.ts +1 -0
  43. package/dist/core/hook.d.ts +134 -0
  44. package/dist/core/hook.test.d.ts +1 -0
  45. package/dist/core/isAtom.d.ts +9 -0
  46. package/dist/core/isPromiseLike.d.ts +9 -0
  47. package/dist/core/isPromiseLike.test.d.ts +1 -0
  48. package/dist/core/onCreateHook.d.ts +79 -0
  49. package/dist/core/promiseCache.d.ts +134 -0
  50. package/dist/core/promiseCache.test.d.ts +1 -0
  51. package/dist/core/scheduleNotifyHook.d.ts +51 -0
  52. package/dist/core/select.d.ts +151 -0
  53. package/dist/core/selector.test.d.ts +1 -0
  54. package/dist/core/types.d.ts +279 -0
  55. package/dist/core/withUse.d.ts +38 -0
  56. package/dist/core/withUse.test.d.ts +1 -0
  57. package/dist/index-2ok7ilik.js +1217 -0
  58. package/dist/index-B_5SFzfl.cjs +1 -0
  59. package/dist/index.cjs +1 -0
  60. package/dist/index.d.ts +14 -0
  61. package/dist/index.js +20 -0
  62. package/dist/index.test.d.ts +1 -0
  63. package/dist/react/index.cjs +30 -0
  64. package/dist/react/index.d.ts +7 -0
  65. package/dist/react/index.js +823 -0
  66. package/dist/react/rx.d.ts +250 -0
  67. package/dist/react/rx.test.d.ts +1 -0
  68. package/dist/react/strictModeTest.d.ts +10 -0
  69. package/dist/react/useAction.d.ts +381 -0
  70. package/dist/react/useAction.test.d.ts +1 -0
  71. package/dist/react/useStable.d.ts +183 -0
  72. package/dist/react/useStable.test.d.ts +1 -0
  73. package/dist/react/useValue.d.ts +134 -0
  74. package/dist/react/useValue.test.d.ts +1 -0
  75. package/package.json +57 -0
  76. package/scripts/publish.js +198 -0
  77. package/src/core/atom.test.ts +369 -0
  78. package/src/core/atom.ts +189 -0
  79. package/src/core/atomState.test.ts +342 -0
  80. package/src/core/atomState.ts +256 -0
  81. package/src/core/batch.test.ts +257 -0
  82. package/src/core/batch.ts +172 -0
  83. package/src/core/define.test.ts +342 -0
  84. package/src/core/define.ts +243 -0
  85. package/src/core/derived.test.ts +381 -0
  86. package/src/core/derived.ts +339 -0
  87. package/src/core/effect.test.ts +196 -0
  88. package/src/core/effect.ts +184 -0
  89. package/src/core/emitter.test.ts +364 -0
  90. package/src/core/emitter.ts +392 -0
  91. package/src/core/equality.test.ts +392 -0
  92. package/src/core/equality.ts +182 -0
  93. package/src/core/hook.test.ts +227 -0
  94. package/src/core/hook.ts +177 -0
  95. package/src/core/isAtom.ts +27 -0
  96. package/src/core/isPromiseLike.test.ts +72 -0
  97. package/src/core/isPromiseLike.ts +16 -0
  98. package/src/core/onCreateHook.ts +92 -0
  99. package/src/core/promiseCache.test.ts +239 -0
  100. package/src/core/promiseCache.ts +279 -0
  101. package/src/core/scheduleNotifyHook.ts +53 -0
  102. package/src/core/select.ts +454 -0
  103. package/src/core/selector.test.ts +257 -0
  104. package/src/core/types.ts +311 -0
  105. package/src/core/withUse.test.ts +249 -0
  106. package/src/core/withUse.ts +56 -0
  107. package/src/index.test.ts +80 -0
  108. package/src/index.ts +51 -0
  109. package/src/react/index.ts +20 -0
  110. package/src/react/rx.test.tsx +416 -0
  111. package/src/react/rx.tsx +300 -0
  112. package/src/react/strictModeTest.tsx +71 -0
  113. package/src/react/useAction.test.ts +989 -0
  114. package/src/react/useAction.ts +605 -0
  115. package/src/react/useStable.test.ts +553 -0
  116. package/src/react/useStable.ts +288 -0
  117. package/src/react/useValue.test.ts +182 -0
  118. package/src/react/useValue.ts +261 -0
  119. package/tsconfig.json +9 -0
  120. package/v2.md +725 -0
  121. package/vite.config.ts +39 -0
@@ -0,0 +1,300 @@
1
+ import { memo } from "react";
2
+ import { Atom, Equality } from "../core/types";
3
+ import { useValue } from "./useValue";
4
+ import { shallowEqual } from "../core/equality";
5
+ import { isAtom } from "../core/isAtom";
6
+ import { ContextSelectorFn } from "../core/select";
7
+
8
+ /**
9
+ * Reactive inline component that renders atom values directly in JSX.
10
+ *
11
+ * `rx` is a convenience wrapper around `useValue` that returns a memoized
12
+ * React component instead of a value. This enables fine-grained reactivity
13
+ * without creating separate components for each reactive value.
14
+ *
15
+ * ## IMPORTANT: Selector Must Return Synchronous Value
16
+ *
17
+ * **The selector function MUST NOT be async or return a Promise.**
18
+ *
19
+ * ```tsx
20
+ * // ❌ WRONG - Don't use async function
21
+ * rx(async ({ get }) => {
22
+ * const data = await fetch('/api');
23
+ * return data.name;
24
+ * });
25
+ *
26
+ * // ❌ WRONG - Don't return a Promise
27
+ * rx(({ get }) => fetch('/api').then(r => r.json()));
28
+ *
29
+ * // ✅ CORRECT - Create async atom and read with get()
30
+ * const data$ = atom(fetch('/api').then(r => r.json()));
31
+ * rx(({ get }) => get(data$).name); // Suspends until resolved
32
+ * ```
33
+ *
34
+ * ## Why Use `rx`?
35
+ *
36
+ * Without `rx`, you need a separate component to subscribe to an atom:
37
+ * ```tsx
38
+ * function PostsList() {
39
+ * const posts = useValue(postsAtom);
40
+ * return posts.map((post) => <Post post={post} />);
41
+ * }
42
+ *
43
+ * function Page() {
44
+ * return (
45
+ * <Suspense fallback={<Loading />}>
46
+ * <PostsList />
47
+ * </Suspense>
48
+ * );
49
+ * }
50
+ * ```
51
+ *
52
+ * With `rx`, you can subscribe inline:
53
+ * ```tsx
54
+ * function Page() {
55
+ * return (
56
+ * <Suspense fallback={<Loading />}>
57
+ * {rx(({ get }) =>
58
+ * get(postsAtom).map((post) => <Post post={post} />)
59
+ * )}
60
+ * </Suspense>
61
+ * );
62
+ * }
63
+ * ```
64
+ *
65
+ * ## Key Benefits
66
+ *
67
+ * 1. **Fine-grained updates**: Only the `rx` component re-renders when the atom changes,
68
+ * not the parent component
69
+ * 2. **Less boilerplate**: No need to create single-purpose wrapper components
70
+ * 3. **Colocation**: Keep reactive logic inline where it's used
71
+ * 4. **Memoized**: Uses `React.memo` to prevent unnecessary re-renders
72
+ * 5. **Type-safe**: Full TypeScript support with proper type inference
73
+ *
74
+ * ## Async Atoms (Suspense-Style API)
75
+ *
76
+ * `rx` inherits the Suspense-style API from `useValue`:
77
+ * - **Loading state**: The getter throws a Promise (triggers Suspense)
78
+ * - **Error state**: The getter throws the error (triggers ErrorBoundary)
79
+ * - **Resolved state**: The getter returns the value
80
+ *
81
+ * For async atoms, you MUST wrap with `<Suspense>` and `<ErrorBoundary>`:
82
+ * ```tsx
83
+ * function App() {
84
+ * return (
85
+ * <ErrorBoundary fallback={<div>Error!</div>}>
86
+ * <Suspense fallback={<div>Loading...</div>}>
87
+ * {rx(({ get }) => get(userAtom).name)}
88
+ * </Suspense>
89
+ * </ErrorBoundary>
90
+ * );
91
+ * }
92
+ * ```
93
+ *
94
+ * Or catch errors in the selector to handle loading/error inline:
95
+ * ```tsx
96
+ * {rx(({ get }) => {
97
+ * try {
98
+ * return get(userAtom).name;
99
+ * } catch {
100
+ * return "Loading...";
101
+ * }
102
+ * })}
103
+ * ```
104
+ *
105
+ * @template T - The type of the selected/derived value
106
+ * @param selector - Context-based selector function with `{ get, all, any, race, settled }`.
107
+ * Must return sync value, not a Promise.
108
+ * @param equals - Equality function or shorthand ("strict", "shallow", "deep").
109
+ * Defaults to "shallow".
110
+ * @returns A React element that renders the selected value
111
+ * @throws Error if selector returns a Promise or PromiseLike
112
+ *
113
+ * @example Shorthand - render atom value directly
114
+ * ```tsx
115
+ * const count = atom(5);
116
+ *
117
+ * function Counter() {
118
+ * return <div>Count: {rx(count)}</div>;
119
+ * }
120
+ * ```
121
+ *
122
+ * @example Context selector - derive a value
123
+ * ```tsx
124
+ * const count = atom(5);
125
+ *
126
+ * function DoubledCounter() {
127
+ * return <div>Doubled: {rx(({ get }) => get(count) * 2)}</div>;
128
+ * }
129
+ * ```
130
+ *
131
+ * @example Multiple atoms
132
+ * ```tsx
133
+ * const firstName = atom("John");
134
+ * const lastName = atom("Doe");
135
+ *
136
+ * function FullName() {
137
+ * return (
138
+ * <div>
139
+ * {rx(({ get }) => `${get(firstName)} ${get(lastName)}`)}
140
+ * </div>
141
+ * );
142
+ * }
143
+ * ```
144
+ *
145
+ * @example Fine-grained updates - parent doesn't re-render
146
+ * ```tsx
147
+ * const count = atom(0);
148
+ *
149
+ * function Parent() {
150
+ * console.log("Parent renders once");
151
+ * return (
152
+ * <div>
153
+ * {rx(count)} {/* Only this re-renders when count changes *\/}
154
+ * <button onClick={() => count.set((n) => n + 1)}>+</button>
155
+ * </div>
156
+ * );
157
+ * }
158
+ * ```
159
+ *
160
+ * @example Multiple subscriptions in one component
161
+ * ```tsx
162
+ * function Dashboard() {
163
+ * return (
164
+ * <div>
165
+ * <header>
166
+ * <Suspense fallback="...">{rx(({ get }) => get(userAtom).name)}</Suspense>
167
+ * </header>
168
+ * <main>
169
+ * <Suspense fallback="...">
170
+ * {rx(({ get }) => get(postsAtom).length)} posts
171
+ * </Suspense>
172
+ * <Suspense fallback="...">
173
+ * {rx(({ get }) => get(notificationsAtom).length)} notifications
174
+ * </Suspense>
175
+ * </main>
176
+ * </div>
177
+ * );
178
+ * }
179
+ * ```
180
+ *
181
+ * @example Conditional dependencies - only subscribes to accessed atoms
182
+ * ```tsx
183
+ * const showDetails = atom(false);
184
+ * const summary = atom("Brief info");
185
+ * const details = atom("Detailed info");
186
+ *
187
+ * function Info() {
188
+ * return (
189
+ * <div>
190
+ * {rx(({ get }) =>
191
+ * get(showDetails) ? get(details) : get(summary)
192
+ * )}
193
+ * </div>
194
+ * );
195
+ * }
196
+ * ```
197
+ *
198
+ * @example With custom equality
199
+ * ```tsx
200
+ * const user = atom({ id: 1, name: "John" });
201
+ *
202
+ * function UserName() {
203
+ * return (
204
+ * <div>
205
+ * {rx(
206
+ * ({ get }) => get(user).name,
207
+ * (a, b) => a === b // Only re-render if name string changes
208
+ * )}
209
+ * </div>
210
+ * );
211
+ * }
212
+ * ```
213
+ *
214
+ * @example Combining multiple async atoms with async utilities
215
+ * ```tsx
216
+ * const userAtom = atom(fetchUser());
217
+ * const postsAtom = atom(fetchPosts());
218
+ *
219
+ * function Dashboard() {
220
+ * return (
221
+ * <Suspense fallback={<Loading />}>
222
+ * {rx(({ all }) => {
223
+ * // Use all() to wait for multiple atoms
224
+ * const [user, posts] = all([userAtom, postsAtom]);
225
+ * return <DashboardContent user={user} posts={posts} />;
226
+ * })}
227
+ * </Suspense>
228
+ * );
229
+ * }
230
+ * ```
231
+ *
232
+ * @example Using settled for partial failures
233
+ * ```tsx
234
+ * const userAtom = atom(fetchUser());
235
+ * const postsAtom = atom(fetchPosts());
236
+ *
237
+ * function Dashboard() {
238
+ * return (
239
+ * <Suspense fallback={<Loading />}>
240
+ * {rx(({ settled }) => {
241
+ * const [userResult, postsResult] = settled([userAtom, postsAtom]);
242
+ * return (
243
+ * <DashboardContent
244
+ * user={userResult.status === 'resolved' ? userResult.value : null}
245
+ * posts={postsResult.status === 'resolved' ? postsResult.value : []}
246
+ * />
247
+ * );
248
+ * })}
249
+ * </Suspense>
250
+ * );
251
+ * }
252
+ * ```
253
+ */
254
+ // Overload: Pass atom directly to get its value (shorthand)
255
+ export function rx<T>(atom: Atom<T>, equals?: Equality<Awaited<T>>): Awaited<T>;
256
+
257
+ // Overload: Context-based selector function
258
+ export function rx<T>(selector: ContextSelectorFn<T>, equals?: Equality<T>): T;
259
+
260
+ export function rx<T>(
261
+ selectorOrAtom: ContextSelectorFn<T> | Atom<T>,
262
+ equals?: Equality<unknown>
263
+ ): T {
264
+ return (
265
+ <Rx
266
+ selectorOrAtom={
267
+ selectorOrAtom as ContextSelectorFn<unknown> | Atom<unknown>
268
+ }
269
+ equals={equals}
270
+ />
271
+ ) as unknown as T;
272
+ }
273
+
274
+ /**
275
+ * Internal memoized component that handles the actual subscription and rendering.
276
+ *
277
+ * Memoized with React.memo to ensure:
278
+ * 1. Parent components don't cause unnecessary re-renders
279
+ * 2. Only atom changes trigger re-renders
280
+ * 3. Props comparison is shallow (selectorOrAtom, equals references)
281
+ *
282
+ * Renders `selected ?? null` to handle null/undefined values gracefully in JSX.
283
+ */
284
+ const Rx = memo(
285
+ function Rx(props: {
286
+ selectorOrAtom: ContextSelectorFn<unknown> | Atom<unknown>;
287
+ equals?: Equality<unknown>;
288
+ }) {
289
+ // Convert atom shorthand to context selector
290
+ const selector: ContextSelectorFn<unknown> = isAtom(props.selectorOrAtom)
291
+ ? ({ get }) => get(props.selectorOrAtom as Atom<unknown>)
292
+ : (props.selectorOrAtom as ContextSelectorFn<unknown>);
293
+
294
+ const selected = useValue(selector, props.equals);
295
+ return <>{selected ?? null}</>;
296
+ },
297
+ (prev, next) =>
298
+ shallowEqual(prev.selectorOrAtom, next.selectorOrAtom) &&
299
+ prev.equals === next.equals
300
+ );
@@ -0,0 +1,71 @@
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
+ ];