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.
- package/README.md +1666 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/clover.xml +1440 -0
- package/coverage/coverage-final.json +14 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +131 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/src/core/atom.ts.html +889 -0
- package/coverage/src/core/batch.ts.html +223 -0
- package/coverage/src/core/define.ts.html +805 -0
- package/coverage/src/core/emitter.ts.html +919 -0
- package/coverage/src/core/equality.ts.html +631 -0
- package/coverage/src/core/hook.ts.html +460 -0
- package/coverage/src/core/index.html +281 -0
- package/coverage/src/core/isAtom.ts.html +100 -0
- package/coverage/src/core/isPromiseLike.ts.html +133 -0
- package/coverage/src/core/onCreateHook.ts.html +136 -0
- package/coverage/src/core/scheduleNotifyHook.ts.html +94 -0
- package/coverage/src/core/types.ts.html +523 -0
- package/coverage/src/core/withUse.ts.html +253 -0
- package/coverage/src/index.html +116 -0
- package/coverage/src/index.ts.html +106 -0
- package/dist/core/atom.d.ts +63 -0
- package/dist/core/atom.test.d.ts +1 -0
- package/dist/core/atomState.d.ts +104 -0
- package/dist/core/atomState.test.d.ts +1 -0
- package/dist/core/batch.d.ts +126 -0
- package/dist/core/batch.test.d.ts +1 -0
- package/dist/core/define.d.ts +173 -0
- package/dist/core/define.test.d.ts +1 -0
- package/dist/core/derived.d.ts +102 -0
- package/dist/core/derived.test.d.ts +1 -0
- package/dist/core/effect.d.ts +120 -0
- package/dist/core/effect.test.d.ts +1 -0
- package/dist/core/emitter.d.ts +237 -0
- package/dist/core/emitter.test.d.ts +1 -0
- package/dist/core/equality.d.ts +62 -0
- package/dist/core/equality.test.d.ts +1 -0
- package/dist/core/hook.d.ts +134 -0
- package/dist/core/hook.test.d.ts +1 -0
- package/dist/core/isAtom.d.ts +9 -0
- package/dist/core/isPromiseLike.d.ts +9 -0
- package/dist/core/isPromiseLike.test.d.ts +1 -0
- package/dist/core/onCreateHook.d.ts +79 -0
- package/dist/core/promiseCache.d.ts +134 -0
- package/dist/core/promiseCache.test.d.ts +1 -0
- package/dist/core/scheduleNotifyHook.d.ts +51 -0
- package/dist/core/select.d.ts +151 -0
- package/dist/core/selector.test.d.ts +1 -0
- package/dist/core/types.d.ts +279 -0
- package/dist/core/withUse.d.ts +38 -0
- package/dist/core/withUse.test.d.ts +1 -0
- package/dist/index-2ok7ilik.js +1217 -0
- package/dist/index-B_5SFzfl.cjs +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +20 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/react/index.cjs +30 -0
- package/dist/react/index.d.ts +7 -0
- package/dist/react/index.js +823 -0
- package/dist/react/rx.d.ts +250 -0
- package/dist/react/rx.test.d.ts +1 -0
- package/dist/react/strictModeTest.d.ts +10 -0
- package/dist/react/useAction.d.ts +381 -0
- package/dist/react/useAction.test.d.ts +1 -0
- package/dist/react/useStable.d.ts +183 -0
- package/dist/react/useStable.test.d.ts +1 -0
- package/dist/react/useValue.d.ts +134 -0
- package/dist/react/useValue.test.d.ts +1 -0
- package/package.json +57 -0
- package/scripts/publish.js +198 -0
- package/src/core/atom.test.ts +369 -0
- package/src/core/atom.ts +189 -0
- package/src/core/atomState.test.ts +342 -0
- package/src/core/atomState.ts +256 -0
- package/src/core/batch.test.ts +257 -0
- package/src/core/batch.ts +172 -0
- package/src/core/define.test.ts +342 -0
- package/src/core/define.ts +243 -0
- package/src/core/derived.test.ts +381 -0
- package/src/core/derived.ts +339 -0
- package/src/core/effect.test.ts +196 -0
- package/src/core/effect.ts +184 -0
- package/src/core/emitter.test.ts +364 -0
- package/src/core/emitter.ts +392 -0
- package/src/core/equality.test.ts +392 -0
- package/src/core/equality.ts +182 -0
- package/src/core/hook.test.ts +227 -0
- package/src/core/hook.ts +177 -0
- package/src/core/isAtom.ts +27 -0
- package/src/core/isPromiseLike.test.ts +72 -0
- package/src/core/isPromiseLike.ts +16 -0
- package/src/core/onCreateHook.ts +92 -0
- package/src/core/promiseCache.test.ts +239 -0
- package/src/core/promiseCache.ts +279 -0
- package/src/core/scheduleNotifyHook.ts +53 -0
- package/src/core/select.ts +454 -0
- package/src/core/selector.test.ts +257 -0
- package/src/core/types.ts +311 -0
- package/src/core/withUse.test.ts +249 -0
- package/src/core/withUse.ts +56 -0
- package/src/index.test.ts +80 -0
- package/src/index.ts +51 -0
- package/src/react/index.ts +20 -0
- package/src/react/rx.test.tsx +416 -0
- package/src/react/rx.tsx +300 -0
- package/src/react/strictModeTest.tsx +71 -0
- package/src/react/useAction.test.ts +989 -0
- package/src/react/useAction.ts +605 -0
- package/src/react/useStable.test.ts +553 -0
- package/src/react/useStable.ts +288 -0
- package/src/react/useValue.test.ts +182 -0
- package/src/react/useValue.ts +261 -0
- package/tsconfig.json +9 -0
- package/v2.md +725 -0
- package/vite.config.ts +39 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { useSyncExternalStore, useCallback, useRef } from "react";
|
|
2
|
+
import { select, ContextSelectorFn } from "../core/select";
|
|
3
|
+
import { resolveEquality } from "../core/equality";
|
|
4
|
+
import { Atom, Equality } from "../core/types";
|
|
5
|
+
import { isAtom } from "../core/isAtom";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* React hook that selects/derives a value from atom(s) with automatic subscriptions.
|
|
9
|
+
*
|
|
10
|
+
* Uses `useSyncExternalStore` for proper React 18+ concurrent mode support.
|
|
11
|
+
* Only subscribes to atoms that are actually accessed during selection.
|
|
12
|
+
*
|
|
13
|
+
* ## IMPORTANT: Selector Must Return Synchronous Value
|
|
14
|
+
*
|
|
15
|
+
* **The selector function MUST NOT be async or return a Promise.**
|
|
16
|
+
*
|
|
17
|
+
* ```tsx
|
|
18
|
+
* // ❌ WRONG - Don't use async function
|
|
19
|
+
* useValue(async ({ get }) => {
|
|
20
|
+
* const data = await fetch('/api');
|
|
21
|
+
* return data;
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* // ❌ WRONG - Don't return a Promise
|
|
25
|
+
* useValue(({ get }) => fetch('/api').then(r => r.json()));
|
|
26
|
+
*
|
|
27
|
+
* // ✅ CORRECT - Create async atom and read with get()
|
|
28
|
+
* const data$ = atom(fetch('/api').then(r => r.json()));
|
|
29
|
+
* useValue(({ get }) => get(data$)); // Suspends until resolved
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* ## IMPORTANT: Suspense-Style API
|
|
33
|
+
*
|
|
34
|
+
* This hook uses a **Suspense-style API** for async atoms:
|
|
35
|
+
* - When an atom is **loading**, the getter throws a Promise (suspends)
|
|
36
|
+
* - When an atom has an **error**, the getter throws the error
|
|
37
|
+
* - When an atom is **resolved**, the getter returns the value
|
|
38
|
+
*
|
|
39
|
+
* This means:
|
|
40
|
+
* - **You MUST wrap components with `<Suspense>`** to handle loading states
|
|
41
|
+
* - **You MUST wrap components with `<ErrorBoundary>`** to handle errors
|
|
42
|
+
*
|
|
43
|
+
* ## Alternative: Using staleValue for Non-Suspense
|
|
44
|
+
*
|
|
45
|
+
* If you want to show loading states without Suspense:
|
|
46
|
+
*
|
|
47
|
+
* ```tsx
|
|
48
|
+
* function MyComponent() {
|
|
49
|
+
* // Access staleValue directly - always has a value (with fallback)
|
|
50
|
+
* const count = myDerivedAtom$.staleValue;
|
|
51
|
+
* const isLoading = isPending(myDerivedAtom$.value);
|
|
52
|
+
*
|
|
53
|
+
* return (
|
|
54
|
+
* <div>
|
|
55
|
+
* {isLoading && <Spinner />}
|
|
56
|
+
* Count: {count}
|
|
57
|
+
* </div>
|
|
58
|
+
* );
|
|
59
|
+
* }
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* @template T - The type of the selected value
|
|
63
|
+
* @param selectorOrAtom - Atom or context-based selector function (must return sync value)
|
|
64
|
+
* @param equals - Equality function or shorthand. Defaults to "shallow"
|
|
65
|
+
* @returns The selected value (Awaited<T>)
|
|
66
|
+
* @throws Error if selector returns a Promise or PromiseLike
|
|
67
|
+
*
|
|
68
|
+
* @example Single atom (shorthand)
|
|
69
|
+
* ```tsx
|
|
70
|
+
* const count = atom(5);
|
|
71
|
+
*
|
|
72
|
+
* function Counter() {
|
|
73
|
+
* const value = useValue(count);
|
|
74
|
+
* return <div>{value}</div>;
|
|
75
|
+
* }
|
|
76
|
+
* ```
|
|
77
|
+
*
|
|
78
|
+
* @example With selector
|
|
79
|
+
* ```tsx
|
|
80
|
+
* const count = atom(5);
|
|
81
|
+
*
|
|
82
|
+
* function Counter() {
|
|
83
|
+
* const doubled = useValue(({ get }) => get(count) * 2);
|
|
84
|
+
* return <div>{doubled}</div>;
|
|
85
|
+
* }
|
|
86
|
+
* ```
|
|
87
|
+
*
|
|
88
|
+
* @example Multiple atoms
|
|
89
|
+
* ```tsx
|
|
90
|
+
* const firstName = atom("John");
|
|
91
|
+
* const lastName = atom("Doe");
|
|
92
|
+
*
|
|
93
|
+
* function FullName() {
|
|
94
|
+
* const fullName = useValue(({ get }) =>
|
|
95
|
+
* `${get(firstName)} ${get(lastName)}`
|
|
96
|
+
* );
|
|
97
|
+
* return <div>{fullName}</div>;
|
|
98
|
+
* }
|
|
99
|
+
* ```
|
|
100
|
+
*
|
|
101
|
+
* @example Async atom with Suspense
|
|
102
|
+
* ```tsx
|
|
103
|
+
* const userAtom = atom(fetchUser());
|
|
104
|
+
*
|
|
105
|
+
* function UserProfile() {
|
|
106
|
+
* const user = useValue(({ get }) => get(userAtom));
|
|
107
|
+
* return <div>{user.name}</div>;
|
|
108
|
+
* }
|
|
109
|
+
*
|
|
110
|
+
* // MUST wrap with Suspense and ErrorBoundary
|
|
111
|
+
* function App() {
|
|
112
|
+
* return (
|
|
113
|
+
* <ErrorBoundary fallback={<div>Error!</div>}>
|
|
114
|
+
* <Suspense fallback={<div>Loading...</div>}>
|
|
115
|
+
* <UserProfile />
|
|
116
|
+
* </Suspense>
|
|
117
|
+
* </ErrorBoundary>
|
|
118
|
+
* );
|
|
119
|
+
* }
|
|
120
|
+
* ```
|
|
121
|
+
*
|
|
122
|
+
* @example Using all() for multiple async atoms
|
|
123
|
+
* ```tsx
|
|
124
|
+
* const userAtom = atom(fetchUser());
|
|
125
|
+
* const postsAtom = atom(fetchPosts());
|
|
126
|
+
*
|
|
127
|
+
* function Dashboard() {
|
|
128
|
+
* const data = useValue(({ all }) => {
|
|
129
|
+
* const [user, posts] = all(userAtom, postsAtom);
|
|
130
|
+
* return { user, posts };
|
|
131
|
+
* });
|
|
132
|
+
*
|
|
133
|
+
* return <DashboardContent user={data.user} posts={data.posts} />;
|
|
134
|
+
* }
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
// Overload: Pass atom directly
|
|
138
|
+
export function useValue<T>(
|
|
139
|
+
atom: Atom<T>,
|
|
140
|
+
equals?: Equality<Awaited<T>>
|
|
141
|
+
): Awaited<T>;
|
|
142
|
+
|
|
143
|
+
// Overload: Context-based selector function
|
|
144
|
+
export function useValue<T>(
|
|
145
|
+
selector: ContextSelectorFn<T>,
|
|
146
|
+
equals?: Equality<T>
|
|
147
|
+
): T;
|
|
148
|
+
|
|
149
|
+
export function useValue<T>(
|
|
150
|
+
selectorOrAtom: ContextSelectorFn<T> | Atom<T>,
|
|
151
|
+
equals?: Equality<T>
|
|
152
|
+
): T {
|
|
153
|
+
// Convert atom shorthand to context selector
|
|
154
|
+
const selector: ContextSelectorFn<T> = isAtom(selectorOrAtom)
|
|
155
|
+
? ({ get }) => get(selectorOrAtom as Atom<T>) as T
|
|
156
|
+
: (selectorOrAtom as ContextSelectorFn<T>);
|
|
157
|
+
|
|
158
|
+
// Default to shallow equality
|
|
159
|
+
const eq = resolveEquality((equals as Equality<unknown>) ?? "shallow");
|
|
160
|
+
|
|
161
|
+
// Store selector in ref to avoid recreating callbacks
|
|
162
|
+
const selectorRef = useRef(selector);
|
|
163
|
+
const eqRef = useRef(eq);
|
|
164
|
+
|
|
165
|
+
// Update refs on each render
|
|
166
|
+
selectorRef.current = selector;
|
|
167
|
+
eqRef.current = eq;
|
|
168
|
+
|
|
169
|
+
// Track current dependencies and their unsubscribe functions
|
|
170
|
+
const subscriptionsRef = useRef<Map<Atom<unknown>, VoidFunction>>(new Map());
|
|
171
|
+
const dependenciesRef = useRef<Set<Atom<unknown>>>(new Set());
|
|
172
|
+
|
|
173
|
+
// Cache the last snapshot
|
|
174
|
+
const snapshotRef = useRef<{ value: T; initialized: boolean }>({
|
|
175
|
+
value: undefined as T,
|
|
176
|
+
initialized: false,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get the current snapshot by running the selector.
|
|
181
|
+
*/
|
|
182
|
+
const getSnapshot = useCallback(() => {
|
|
183
|
+
const result = select(selectorRef.current);
|
|
184
|
+
|
|
185
|
+
// Update dependencies
|
|
186
|
+
dependenciesRef.current = result.dependencies;
|
|
187
|
+
|
|
188
|
+
// Handle Suspense-style states
|
|
189
|
+
if (result.promise !== undefined) {
|
|
190
|
+
// Loading state - throw Promise
|
|
191
|
+
throw result.promise;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (result.error !== undefined) {
|
|
195
|
+
// Error state - throw error
|
|
196
|
+
throw result.error;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Success - check equality and update cache
|
|
200
|
+
const newValue = result.value as T;
|
|
201
|
+
|
|
202
|
+
if (
|
|
203
|
+
!snapshotRef.current.initialized ||
|
|
204
|
+
!eqRef.current(newValue, snapshotRef.current.value)
|
|
205
|
+
) {
|
|
206
|
+
snapshotRef.current = { value: newValue, initialized: true };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return snapshotRef.current.value;
|
|
210
|
+
}, []);
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Subscribe to atom changes.
|
|
214
|
+
*/
|
|
215
|
+
const subscribe = useCallback((onStoreChange: () => void) => {
|
|
216
|
+
const subscriptions = subscriptionsRef.current;
|
|
217
|
+
|
|
218
|
+
const updateSubscriptions = () => {
|
|
219
|
+
const currentDeps = dependenciesRef.current;
|
|
220
|
+
|
|
221
|
+
// Unsubscribe from atoms no longer dependencies
|
|
222
|
+
for (const [atom, unsubscribe] of subscriptions) {
|
|
223
|
+
if (!currentDeps.has(atom)) {
|
|
224
|
+
unsubscribe();
|
|
225
|
+
subscriptions.delete(atom);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Subscribe to new dependencies
|
|
230
|
+
for (const atom of currentDeps) {
|
|
231
|
+
if (!subscriptions.has(atom)) {
|
|
232
|
+
const unsubscribe = atom.on(() => {
|
|
233
|
+
// Re-run selector to update dependencies
|
|
234
|
+
const result = select(selectorRef.current);
|
|
235
|
+
dependenciesRef.current = result.dependencies;
|
|
236
|
+
|
|
237
|
+
// Update subscriptions if dependencies changed
|
|
238
|
+
updateSubscriptions();
|
|
239
|
+
|
|
240
|
+
// Notify React
|
|
241
|
+
onStoreChange();
|
|
242
|
+
});
|
|
243
|
+
subscriptions.set(atom, unsubscribe);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// Initial subscription setup
|
|
249
|
+
updateSubscriptions();
|
|
250
|
+
|
|
251
|
+
// Cleanup function
|
|
252
|
+
return () => {
|
|
253
|
+
for (const unsubscribe of subscriptions.values()) {
|
|
254
|
+
unsubscribe();
|
|
255
|
+
}
|
|
256
|
+
subscriptions.clear();
|
|
257
|
+
};
|
|
258
|
+
}, []);
|
|
259
|
+
|
|
260
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
261
|
+
}
|