atomirx 0.0.2 → 0.0.5
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 +868 -161
- package/coverage/src/core/onCreateHook.ts.html +72 -70
- package/dist/core/atom.d.ts +83 -6
- package/dist/core/batch.d.ts +3 -3
- package/dist/core/derived.d.ts +69 -22
- package/dist/core/effect.d.ts +52 -52
- package/dist/core/getAtomState.d.ts +29 -0
- package/dist/core/hook.d.ts +1 -1
- package/dist/core/onCreateHook.d.ts +37 -23
- package/dist/core/onErrorHook.d.ts +49 -0
- package/dist/core/promiseCache.d.ts +23 -32
- package/dist/core/select.d.ts +208 -29
- package/dist/core/types.d.ts +107 -22
- package/dist/core/withReady.d.ts +115 -0
- package/dist/core/withReady.test.d.ts +1 -0
- package/dist/index-CBVj1kSj.js +1350 -0
- package/dist/index-Cxk9v0um.cjs +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +12 -8
- package/dist/index.js +18 -15
- package/dist/react/index.cjs +10 -10
- package/dist/react/index.d.ts +2 -1
- package/dist/react/index.js +422 -377
- package/dist/react/rx.d.ts +114 -25
- package/dist/react/useAction.d.ts +5 -4
- package/dist/react/{useValue.d.ts → useSelector.d.ts} +56 -25
- package/dist/react/useSelector.test.d.ts +1 -0
- package/package.json +1 -1
- package/src/core/atom.test.ts +307 -43
- package/src/core/atom.ts +144 -22
- package/src/core/batch.test.ts +10 -10
- package/src/core/batch.ts +3 -3
- package/src/core/define.test.ts +12 -11
- package/src/core/define.ts +1 -1
- package/src/core/derived.test.ts +906 -72
- package/src/core/derived.ts +192 -81
- package/src/core/effect.test.ts +651 -45
- package/src/core/effect.ts +102 -98
- package/src/core/getAtomState.ts +69 -0
- package/src/core/hook.test.ts +5 -5
- package/src/core/hook.ts +1 -1
- package/src/core/onCreateHook.ts +38 -23
- package/src/core/onErrorHook.test.ts +350 -0
- package/src/core/onErrorHook.ts +52 -0
- package/src/core/promiseCache.test.ts +5 -3
- package/src/core/promiseCache.ts +76 -71
- package/src/core/select.ts +405 -130
- package/src/core/selector.test.ts +574 -32
- package/src/core/types.ts +107 -29
- package/src/core/withReady.test.ts +534 -0
- package/src/core/withReady.ts +191 -0
- package/src/core/withUse.ts +1 -1
- package/src/index.test.ts +4 -4
- package/src/index.ts +21 -7
- package/src/react/index.ts +2 -1
- package/src/react/rx.test.tsx +173 -18
- package/src/react/rx.tsx +274 -43
- package/src/react/useAction.test.ts +12 -14
- package/src/react/useAction.ts +11 -9
- package/src/react/{useValue.test.ts → useSelector.test.ts} +16 -16
- package/src/react/{useValue.ts → useSelector.ts} +64 -33
- package/v2.md +44 -44
- package/dist/index-2ok7ilik.js +0 -1217
- package/dist/index-B_5SFzfl.cjs +0 -1
- /package/dist/{react/useValue.test.d.ts → core/onErrorHook.test.d.ts} +0 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { isPromiseLike } from "./isPromiseLike";
|
|
2
|
+
import { trackPromise } from "./promiseCache";
|
|
3
|
+
import { SelectContext } from "./select";
|
|
4
|
+
import { AnyFunc, Atom } from "./types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Extension interface that adds `ready()` method to SelectContext.
|
|
8
|
+
* Used in derived atoms and effects to wait for non-null values.
|
|
9
|
+
*/
|
|
10
|
+
export interface WithReadySelectContext {
|
|
11
|
+
/**
|
|
12
|
+
* Wait for an atom to have a non-null/non-undefined value.
|
|
13
|
+
*
|
|
14
|
+
* If the value is null/undefined, the computation suspends until the atom
|
|
15
|
+
* changes to a non-null value, then automatically resumes.
|
|
16
|
+
*
|
|
17
|
+
* **IMPORTANT: Only use in `derived()` or `effect()` context**
|
|
18
|
+
*
|
|
19
|
+
* @param atom - The atom to read and wait for
|
|
20
|
+
* @returns The non-null value (type excludes null | undefined)
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* // Wait for currentArticleId to be set before computing
|
|
25
|
+
* const currentArticle$ = derived(({ ready, read }) => {
|
|
26
|
+
* const id = ready(currentArticleId$); // Suspends if null
|
|
27
|
+
* const cache = read(articleCache$);
|
|
28
|
+
* return cache[id];
|
|
29
|
+
* });
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
ready<T>(
|
|
33
|
+
atom: Atom<T>
|
|
34
|
+
): T extends PromiseLike<any> ? never : Exclude<T, null | undefined>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Wait for a selected value from an atom to be non-null/non-undefined.
|
|
38
|
+
*
|
|
39
|
+
* If the selected value is null/undefined, the computation suspends until the
|
|
40
|
+
* selected value changes to a non-null value, then automatically resumes.
|
|
41
|
+
*
|
|
42
|
+
* **IMPORTANT: Only use in `derived()` or `effect()` context**
|
|
43
|
+
*
|
|
44
|
+
* @param atom - The atom to read
|
|
45
|
+
* @param selector - Function to extract/transform the value
|
|
46
|
+
* @returns The non-null selected value
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* // Wait for user's email to be set
|
|
51
|
+
* const emailDerived$ = derived(({ ready }) => {
|
|
52
|
+
* const email = ready(user$, u => u.email); // Suspends if email is null
|
|
53
|
+
* return `Contact: ${email}`;
|
|
54
|
+
* });
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
ready<T, R>(
|
|
58
|
+
atom: Atom<T>,
|
|
59
|
+
selector: (current: Awaited<T>) => R
|
|
60
|
+
): R extends PromiseLike<any> ? never : Exclude<R, null | undefined>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Execute a function and wait for its result to be non-null/non-undefined.
|
|
64
|
+
*
|
|
65
|
+
* If the function returns null/undefined, the computation suspends until
|
|
66
|
+
* re-executed with a non-null result.
|
|
67
|
+
*
|
|
68
|
+
* **IMPORTANT: Only use in `derived()` or `effect()` context**
|
|
69
|
+
*
|
|
70
|
+
* **NOTE:** This overload is designed for use with async combinators like
|
|
71
|
+
* `all()`, `race()`, `any()`, `settled()` where promises come from stable
|
|
72
|
+
* atom sources. It does NOT support dynamic promise creation (returning a
|
|
73
|
+
* new Promise from the callback). For async selectors that return promises,
|
|
74
|
+
* use `ready(atom$, selector?)` instead.
|
|
75
|
+
*
|
|
76
|
+
* @param fn - Synchronous function to execute and wait for
|
|
77
|
+
* @returns The non-null result (excludes null | undefined)
|
|
78
|
+
* @throws {Error} If the callback returns a Promise
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```ts
|
|
82
|
+
* // Wait for a computed value to be ready
|
|
83
|
+
* const result$ = derived(({ ready, read }) => {
|
|
84
|
+
* const value = ready(() => computeExpensiveValue(read(input$)));
|
|
85
|
+
* return `Result: ${value}`;
|
|
86
|
+
* });
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* // Use with async combinators (all, race, any, settled)
|
|
92
|
+
* const combined$ = derived(({ ready, all }) => {
|
|
93
|
+
* const [user, posts] = ready(() => all(user$, posts$));
|
|
94
|
+
* return { user, posts };
|
|
95
|
+
* });
|
|
96
|
+
* ```
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```ts
|
|
100
|
+
* // For async selectors, use ready(atom$, selector?) instead:
|
|
101
|
+
* const data$ = derived(({ ready }) => {
|
|
102
|
+
* const data = ready(source$, (val) => fetchData(val.id));
|
|
103
|
+
* return data;
|
|
104
|
+
* });
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
ready<T>(
|
|
108
|
+
fn: () => T
|
|
109
|
+
): T extends PromiseLike<any> ? never : Exclude<Awaited<T>, null | undefined>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Internal helper that suspends computation if value is null/undefined.
|
|
114
|
+
*/
|
|
115
|
+
function waitForValue<T>(value: T): any {
|
|
116
|
+
if (value === undefined || value === null) {
|
|
117
|
+
throw new Promise(() => {});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Handle async selectors: when the selector returns a Promise,
|
|
121
|
+
// we track its state and handle suspension/resolution accordingly
|
|
122
|
+
if (isPromiseLike(value)) {
|
|
123
|
+
const p = trackPromise(value);
|
|
124
|
+
|
|
125
|
+
// Promise is still pending - suspend computation by throwing
|
|
126
|
+
// the tracked promise. This enables React Suspense integration.
|
|
127
|
+
if (p.status === "pending") {
|
|
128
|
+
throw p.promise;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Promise resolved successfully - return the resolved value.
|
|
132
|
+
// Note: This bypasses null/undefined checking for async results,
|
|
133
|
+
// allowing async selectors to return any value including null.
|
|
134
|
+
if (p.status === "fulfilled") {
|
|
135
|
+
return p.value;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Promise rejected - propagate the error
|
|
139
|
+
throw p.error;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// For sync values (no selector, or selector returned sync value),
|
|
143
|
+
// check for null/undefined and suspend if not ready
|
|
144
|
+
|
|
145
|
+
return value as Exclude<T, null | undefined>;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Plugin that adds `ready()` method to a SelectContext.
|
|
150
|
+
*
|
|
151
|
+
* `ready()` enables a "reactive suspension" pattern where derived atoms
|
|
152
|
+
* wait for required values before computing. This is useful for:
|
|
153
|
+
*
|
|
154
|
+
* - Route-based entity loading (`/article/:id` - wait for ID to be set)
|
|
155
|
+
* - Authentication-gated content (wait for user to be logged in)
|
|
156
|
+
* - Conditional data dependencies (wait for prerequisite data)
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* ```ts
|
|
160
|
+
* // Used internally by derived() - you don't need to call this directly
|
|
161
|
+
* const result = select((context) => fn(context.use(withReady())));
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
export function withReady() {
|
|
165
|
+
return <TContext extends SelectContext>(
|
|
166
|
+
context: TContext
|
|
167
|
+
): TContext & WithReadySelectContext => {
|
|
168
|
+
return {
|
|
169
|
+
...context,
|
|
170
|
+
ready: (
|
|
171
|
+
atomOrFn: Atom<any> | AnyFunc,
|
|
172
|
+
selector?: (current: any) => any
|
|
173
|
+
): any => {
|
|
174
|
+
if (typeof atomOrFn === "function") {
|
|
175
|
+
const value = atomOrFn();
|
|
176
|
+
if (isPromiseLike(value)) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
"ready(callback) overload does not support async callbacks. Use ready(atom, selector?) instead."
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
return waitForValue(value);
|
|
182
|
+
}
|
|
183
|
+
const value = context.read(atomOrFn);
|
|
184
|
+
// we allow selector to return a promise, and wait for that promise if it is not resolved yet
|
|
185
|
+
const selected = selector ? selector(value) : value;
|
|
186
|
+
|
|
187
|
+
return waitForValue(selected);
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
};
|
|
191
|
+
}
|
package/src/core/withUse.ts
CHANGED
|
@@ -38,7 +38,7 @@ import type { Pipeable } from "./types";
|
|
|
38
38
|
*/
|
|
39
39
|
export function withUse<TSource extends object>(source: TSource) {
|
|
40
40
|
return Object.assign(source, {
|
|
41
|
-
use<TNew = void>(plugin: (source: TSource) => TNew): any {
|
|
41
|
+
use<TNew = void>(plugin: (source: NoInfer<TSource>) => TNew): any {
|
|
42
42
|
const result = plugin(source);
|
|
43
43
|
// Void/falsy: return original source (side-effect only plugins)
|
|
44
44
|
if (!result) return source;
|
package/src/index.test.ts
CHANGED
|
@@ -17,7 +17,7 @@ describe("atomirx exports", () => {
|
|
|
17
17
|
it("should export atom", () => {
|
|
18
18
|
expect(typeof atom).toBe("function");
|
|
19
19
|
const count = atom(0);
|
|
20
|
-
expect(count.
|
|
20
|
+
expect(count.get()).toBe(0);
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
it("should export batch", () => {
|
|
@@ -31,8 +31,8 @@ describe("atomirx exports", () => {
|
|
|
31
31
|
it("should export derived", async () => {
|
|
32
32
|
expect(typeof derived).toBe("function");
|
|
33
33
|
const count = atom(5);
|
|
34
|
-
const doubled = derived(({
|
|
35
|
-
expect(await doubled.
|
|
34
|
+
const doubled = derived(({ read }) => read(count) * 2);
|
|
35
|
+
expect(await doubled.get()).toBe(10);
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
it("should export effect", () => {
|
|
@@ -53,7 +53,7 @@ describe("atomirx exports", () => {
|
|
|
53
53
|
it("should export isDerived", () => {
|
|
54
54
|
expect(typeof isDerived).toBe("function");
|
|
55
55
|
const count = atom(0);
|
|
56
|
-
const doubled = derived(({
|
|
56
|
+
const doubled = derived(({ read }) => read(count) * 2);
|
|
57
57
|
expect(isDerived(count)).toBe(false);
|
|
58
58
|
expect(isDerived(doubled)).toBe(true);
|
|
59
59
|
});
|
package/src/index.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
// Core
|
|
2
|
-
export { atom } from "./core/atom";
|
|
2
|
+
export { atom, readonly } from "./core/atom";
|
|
3
3
|
export { batch } from "./core/batch";
|
|
4
4
|
export { define } from "./core/define";
|
|
5
|
-
export { derived } from "./core/derived";
|
|
6
|
-
export { effect } from "./core/effect";
|
|
5
|
+
export { derived, type DerivedContext } from "./core/derived";
|
|
6
|
+
export { effect, type EffectContext } from "./core/effect";
|
|
7
7
|
export { emitter } from "./core/emitter";
|
|
8
8
|
export { isAtom, isDerived } from "./core/isAtom";
|
|
9
9
|
export { select, AllAtomsRejectedError } from "./core/select";
|
|
10
10
|
|
|
11
11
|
// Promise utilities
|
|
12
|
+
export { getAtomState } from "./core/getAtomState";
|
|
12
13
|
export {
|
|
13
|
-
getAtomState,
|
|
14
14
|
isPending,
|
|
15
15
|
isFulfilled,
|
|
16
16
|
isRejected,
|
|
@@ -33,19 +33,33 @@ export type {
|
|
|
33
33
|
Equality,
|
|
34
34
|
EqualityShorthand,
|
|
35
35
|
Getter,
|
|
36
|
+
KeyedResult,
|
|
36
37
|
MutableAtom,
|
|
37
38
|
MutableAtomMeta,
|
|
38
39
|
Pipeable,
|
|
40
|
+
SelectStateResult,
|
|
39
41
|
SettledResult,
|
|
40
42
|
} from "./core/types";
|
|
41
43
|
|
|
42
44
|
export { onCreateHook } from "./core/onCreateHook";
|
|
43
|
-
export type {
|
|
45
|
+
export type {
|
|
46
|
+
CreateInfo,
|
|
47
|
+
MutableInfo,
|
|
48
|
+
DerivedInfo,
|
|
49
|
+
EffectInfo,
|
|
50
|
+
ModuleInfo,
|
|
51
|
+
} from "./core/onCreateHook";
|
|
52
|
+
|
|
53
|
+
export { onErrorHook } from "./core/onErrorHook";
|
|
54
|
+
export type { ErrorInfo } from "./core/onErrorHook";
|
|
44
55
|
|
|
45
56
|
export type {
|
|
46
57
|
SelectContext,
|
|
47
58
|
SelectResult,
|
|
48
|
-
ContextSelectorFn,
|
|
59
|
+
ReactiveSelector as ContextSelectorFn,
|
|
60
|
+
SafeResult,
|
|
49
61
|
} from "./core/select";
|
|
50
62
|
|
|
51
|
-
export type { PromiseState } from "./core/promiseCache";
|
|
63
|
+
export type { PromiseState, CombinedPromiseMeta } from "./core/promiseCache";
|
|
64
|
+
|
|
65
|
+
export { promisesEqual } from "./core/promiseCache";
|
package/src/react/index.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { useSelector } from "./useSelector";
|
|
2
2
|
export { useStable } from "./useStable";
|
|
3
3
|
export type { UseStableResult } from "./useStable";
|
|
4
4
|
export { useAction } from "./useAction";
|
|
5
5
|
export { rx } from "./rx";
|
|
6
|
+
export type { RxOptions } from "./rx";
|
|
6
7
|
|
|
7
8
|
export type {
|
|
8
9
|
ActionState,
|
package/src/react/rx.test.tsx
CHANGED
|
@@ -24,7 +24,9 @@ describe.each(wrappers)("rx - $mode", ({ render }) => {
|
|
|
24
24
|
it("should render derived value with context selector", () => {
|
|
25
25
|
const count = atom(5);
|
|
26
26
|
|
|
27
|
-
render(
|
|
27
|
+
render(
|
|
28
|
+
<div data-testid="result">{rx(({ read }) => read(count) * 2)}</div>
|
|
29
|
+
);
|
|
28
30
|
|
|
29
31
|
expect(screen.getByTestId("result").textContent).toBe("10");
|
|
30
32
|
});
|
|
@@ -61,7 +63,7 @@ describe.each(wrappers)("rx - $mode", ({ render }) => {
|
|
|
61
63
|
|
|
62
64
|
render(
|
|
63
65
|
<div data-testid="result">
|
|
64
|
-
{rx(({
|
|
66
|
+
{rx(({ read }) => `${read(firstName)} ${read(lastName)}`)}
|
|
65
67
|
</div>
|
|
66
68
|
);
|
|
67
69
|
|
|
@@ -75,7 +77,7 @@ describe.each(wrappers)("rx - $mode", ({ render }) => {
|
|
|
75
77
|
|
|
76
78
|
render(
|
|
77
79
|
<div data-testid="result">
|
|
78
|
-
{rx(({
|
|
80
|
+
{rx(({ read }) => read(a) + read(b) + read(c))}
|
|
79
81
|
</div>
|
|
80
82
|
);
|
|
81
83
|
|
|
@@ -101,7 +103,9 @@ describe.each(wrappers)("rx - $mode", ({ render }) => {
|
|
|
101
103
|
it("should update when source atom changes (context selector)", () => {
|
|
102
104
|
const count = atom(5);
|
|
103
105
|
|
|
104
|
-
render(
|
|
106
|
+
render(
|
|
107
|
+
<div data-testid="result">{rx(({ read }) => read(count) * 2)}</div>
|
|
108
|
+
);
|
|
105
109
|
|
|
106
110
|
expect(screen.getByTestId("result").textContent).toBe("10");
|
|
107
111
|
|
|
@@ -117,7 +121,7 @@ describe.each(wrappers)("rx - $mode", ({ render }) => {
|
|
|
117
121
|
const b = atom(2);
|
|
118
122
|
|
|
119
123
|
render(
|
|
120
|
-
<div data-testid="result">{rx(({
|
|
124
|
+
<div data-testid="result">{rx(({ read }) => read(a) + read(b))}</div>
|
|
121
125
|
);
|
|
122
126
|
|
|
123
127
|
expect(screen.getByTestId("result").textContent).toBe("3");
|
|
@@ -142,8 +146,8 @@ describe.each(wrappers)("rx - $mode", ({ render }) => {
|
|
|
142
146
|
const a = atom(1);
|
|
143
147
|
const b = atom(2);
|
|
144
148
|
|
|
145
|
-
const selectorFn = vi.fn(({
|
|
146
|
-
|
|
149
|
+
const selectorFn = vi.fn(({ read }: SelectContext) =>
|
|
150
|
+
read(flag) ? read(a) : read(b)
|
|
147
151
|
);
|
|
148
152
|
|
|
149
153
|
render(<div data-testid="result">{rx(selectorFn)}</div>);
|
|
@@ -168,7 +172,7 @@ describe.each(wrappers)("rx - $mode", ({ render }) => {
|
|
|
168
172
|
|
|
169
173
|
render(
|
|
170
174
|
<div data-testid="result">
|
|
171
|
-
{rx(({
|
|
175
|
+
{rx(({ read }) => (read(flag) ? read(a) : read(b)))}
|
|
172
176
|
</div>
|
|
173
177
|
);
|
|
174
178
|
|
|
@@ -199,7 +203,7 @@ describe.each(wrappers)("rx - $mode", ({ render }) => {
|
|
|
199
203
|
renderCount.current++;
|
|
200
204
|
return (
|
|
201
205
|
<div data-testid="result">
|
|
202
|
-
{rx(({
|
|
206
|
+
{rx(({ read }) => JSON.stringify(read(user)))}
|
|
203
207
|
</div>
|
|
204
208
|
);
|
|
205
209
|
};
|
|
@@ -223,9 +227,9 @@ describe.each(wrappers)("rx - $mode", ({ render }) => {
|
|
|
223
227
|
const user = atom({ name: "John", age: 30 });
|
|
224
228
|
const selectorCallCount = { current: 0 };
|
|
225
229
|
|
|
226
|
-
const selector = ({
|
|
230
|
+
const selector = ({ read }: SelectContext) => {
|
|
227
231
|
selectorCallCount.current++;
|
|
228
|
-
return JSON.stringify(
|
|
232
|
+
return JSON.stringify(read(user));
|
|
229
233
|
};
|
|
230
234
|
|
|
231
235
|
render(<div data-testid="result">{rx(selector, "strict")}</div>);
|
|
@@ -254,7 +258,7 @@ describe.each(wrappers)("rx - $mode", ({ render }) => {
|
|
|
254
258
|
return (
|
|
255
259
|
<div data-testid="result">
|
|
256
260
|
{rx(
|
|
257
|
-
({
|
|
261
|
+
({ read }) => JSON.stringify(read(user)),
|
|
258
262
|
(a, b) => a === b // Compare stringified values
|
|
259
263
|
)}
|
|
260
264
|
</div>
|
|
@@ -285,7 +289,7 @@ describe.each(wrappers)("rx - $mode", ({ render }) => {
|
|
|
285
289
|
const Parent = () => {
|
|
286
290
|
parentRenderCount.current++;
|
|
287
291
|
return (
|
|
288
|
-
<div data-testid="result">{rx(({
|
|
292
|
+
<div data-testid="result">{rx(({ read }) => read(count) * 2)}</div>
|
|
289
293
|
);
|
|
290
294
|
};
|
|
291
295
|
|
|
@@ -317,7 +321,7 @@ describe.each(wrappers)("rx - $mode", ({ render }) => {
|
|
|
317
321
|
const flag = atom(false);
|
|
318
322
|
|
|
319
323
|
render(
|
|
320
|
-
<div data-testid="result">{rx(({
|
|
324
|
+
<div data-testid="result">{rx(({ read }) => String(read(flag)))}</div>
|
|
321
325
|
);
|
|
322
326
|
|
|
323
327
|
expect(screen.getByTestId("result").textContent).toBe("false");
|
|
@@ -352,9 +356,9 @@ describe.each(wrappers)("rx - $mode", ({ render }) => {
|
|
|
352
356
|
|
|
353
357
|
render(
|
|
354
358
|
<div data-testid="result">
|
|
355
|
-
{rx(({
|
|
359
|
+
{rx(({ read }) => {
|
|
356
360
|
try {
|
|
357
|
-
return
|
|
361
|
+
return read(asyncAtom) * 2;
|
|
358
362
|
} catch {
|
|
359
363
|
return "loading";
|
|
360
364
|
}
|
|
@@ -372,7 +376,7 @@ describe.each(wrappers)("rx - $mode", ({ render }) => {
|
|
|
372
376
|
const sourceAtom = atom(5);
|
|
373
377
|
|
|
374
378
|
render(
|
|
375
|
-
<div data-testid="result">{rx(({
|
|
379
|
+
<div data-testid="result">{rx(({ read }) => read(sourceAtom) * 2)}</div>
|
|
376
380
|
);
|
|
377
381
|
|
|
378
382
|
expect(screen.getByTestId("result").textContent).toBe("10");
|
|
@@ -396,7 +400,7 @@ describe.each(wrappers)("rx - $mode", ({ render }) => {
|
|
|
396
400
|
render(
|
|
397
401
|
<div data-testid="result">
|
|
398
402
|
{rx(({ all }) => {
|
|
399
|
-
const [valA, valB] = all(a, b);
|
|
403
|
+
const [valA, valB] = all([a, b]);
|
|
400
404
|
return valA + valB;
|
|
401
405
|
})}
|
|
402
406
|
</div>
|
|
@@ -413,4 +417,155 @@ describe.each(wrappers)("rx - $mode", ({ render }) => {
|
|
|
413
417
|
expect(screen.getByTestId("result").textContent).toBe("12");
|
|
414
418
|
});
|
|
415
419
|
});
|
|
420
|
+
|
|
421
|
+
describe("loading/error options", () => {
|
|
422
|
+
it("should render loading fallback when atom is pending", () => {
|
|
423
|
+
const asyncAtom = atom(new Promise<string>(() => {}));
|
|
424
|
+
|
|
425
|
+
render(
|
|
426
|
+
<div data-testid="result">
|
|
427
|
+
{rx(asyncAtom, { loading: () => <span>Loading...</span> })}
|
|
428
|
+
</div>
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
expect(screen.getByTestId("result").textContent).toBe("Loading...");
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it("should render error fallback when atom has error", async () => {
|
|
435
|
+
const error = new Error("Test error");
|
|
436
|
+
const rejectedPromise = Promise.reject(error);
|
|
437
|
+
rejectedPromise.catch(() => {}); // Prevent unhandled rejection
|
|
438
|
+
const asyncAtom = atom(rejectedPromise);
|
|
439
|
+
|
|
440
|
+
// Wait for promise to be tracked
|
|
441
|
+
await act(async () => {
|
|
442
|
+
await Promise.resolve();
|
|
443
|
+
await Promise.resolve();
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
render(
|
|
447
|
+
<div data-testid="result">
|
|
448
|
+
{rx(asyncAtom, {
|
|
449
|
+
error: ({ error: e }) => <span>Error: {(e as Error).message}</span>,
|
|
450
|
+
})}
|
|
451
|
+
</div>
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
expect(screen.getByTestId("result").textContent).toBe(
|
|
455
|
+
"Error: Test error"
|
|
456
|
+
);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it("should render value when atom resolves with loading option", async () => {
|
|
460
|
+
let resolve: (value: string) => void;
|
|
461
|
+
const promise = new Promise<string>((r) => {
|
|
462
|
+
resolve = r;
|
|
463
|
+
});
|
|
464
|
+
const asyncAtom = atom(promise);
|
|
465
|
+
|
|
466
|
+
const { rerender } = render(
|
|
467
|
+
<div data-testid="result">
|
|
468
|
+
{rx(asyncAtom, {
|
|
469
|
+
loading: () => <span>Loading...</span>,
|
|
470
|
+
})}
|
|
471
|
+
</div>
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
expect(screen.getByTestId("result").textContent).toBe("Loading...");
|
|
475
|
+
|
|
476
|
+
await act(async () => {
|
|
477
|
+
resolve!("Hello");
|
|
478
|
+
await Promise.resolve();
|
|
479
|
+
await Promise.resolve();
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
rerender(
|
|
483
|
+
<div data-testid="result">
|
|
484
|
+
{rx(asyncAtom, {
|
|
485
|
+
loading: () => <span>Loading...</span>,
|
|
486
|
+
})}
|
|
487
|
+
</div>
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
expect(screen.getByTestId("result").textContent).toBe("Hello");
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it("should work with selector function and loading option", () => {
|
|
494
|
+
const asyncAtom = atom(new Promise<number>(() => {}));
|
|
495
|
+
|
|
496
|
+
render(
|
|
497
|
+
<div data-testid="result">
|
|
498
|
+
{rx(({ read }) => read(asyncAtom) * 2, {
|
|
499
|
+
loading: () => <span>Computing...</span>,
|
|
500
|
+
})}
|
|
501
|
+
</div>
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
expect(screen.getByTestId("result").textContent).toBe("Computing...");
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it("should support both loading and error options", async () => {
|
|
508
|
+
const error = new Error("Failed");
|
|
509
|
+
const rejectedPromise = Promise.reject(error);
|
|
510
|
+
rejectedPromise.catch(() => {});
|
|
511
|
+
const asyncAtom = atom(rejectedPromise);
|
|
512
|
+
|
|
513
|
+
await act(async () => {
|
|
514
|
+
await Promise.resolve();
|
|
515
|
+
await Promise.resolve();
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
render(
|
|
519
|
+
<div data-testid="result">
|
|
520
|
+
{rx(asyncAtom, {
|
|
521
|
+
loading: () => <span>Loading...</span>,
|
|
522
|
+
error: ({ error: e }) => (
|
|
523
|
+
<span>Failed: {(e as Error).message}</span>
|
|
524
|
+
),
|
|
525
|
+
})}
|
|
526
|
+
</div>
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
expect(screen.getByTestId("result").textContent).toBe("Failed: Failed");
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it("should pass equality in options object", () => {
|
|
533
|
+
const user = atom({ id: 1, name: "John" });
|
|
534
|
+
const renderSpy = vi.fn();
|
|
535
|
+
|
|
536
|
+
function TestComponent() {
|
|
537
|
+
renderSpy();
|
|
538
|
+
return (
|
|
539
|
+
<div data-testid="result">
|
|
540
|
+
{rx(({ read }) => read(user).name, {
|
|
541
|
+
equals: (a, b) => a === b,
|
|
542
|
+
})}
|
|
543
|
+
</div>
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
render(<TestComponent />);
|
|
548
|
+
expect(screen.getByTestId("result").textContent).toBe("John");
|
|
549
|
+
|
|
550
|
+
// Update with same name
|
|
551
|
+
act(() => {
|
|
552
|
+
user.set({ id: 2, name: "John" });
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
// Name didn't change, so rx content should be same
|
|
556
|
+
expect(screen.getByTestId("result").textContent).toBe("John");
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it("should still work with legacy equality parameter", () => {
|
|
560
|
+
const count = atom(5);
|
|
561
|
+
|
|
562
|
+
render(
|
|
563
|
+
<div data-testid="result">
|
|
564
|
+
{rx(({ read }) => read(count) * 2, "strict")}
|
|
565
|
+
</div>
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
expect(screen.getByTestId("result").textContent).toBe("10");
|
|
569
|
+
});
|
|
570
|
+
});
|
|
416
571
|
});
|