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
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import { renderHook, act, waitFor } from "@testing-library/react";
|
|
3
|
-
import { atom } from "../core/atom";
|
|
4
|
-
import { useSelector } from "./useSelector";
|
|
5
|
-
|
|
6
|
-
describe("useSelector", () => {
|
|
7
|
-
describe("basic functionality", () => {
|
|
8
|
-
it("should read value from sync atom", () => {
|
|
9
|
-
const count$ = atom(5);
|
|
10
|
-
const { result } = renderHook(() => useSelector(count$));
|
|
11
|
-
expect(result.current).toBe(5);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("should update when atom value changes", async () => {
|
|
15
|
-
const count$ = atom(0);
|
|
16
|
-
const { result } = renderHook(() => useSelector(count$));
|
|
17
|
-
|
|
18
|
-
expect(result.current).toBe(0);
|
|
19
|
-
|
|
20
|
-
act(() => {
|
|
21
|
-
count$.set(10);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
await waitFor(() => {
|
|
25
|
-
expect(result.current).toBe(10);
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("should work with object values", () => {
|
|
30
|
-
const user$ = atom({ name: "John", age: 30 });
|
|
31
|
-
const { result } = renderHook(() => useSelector(user$));
|
|
32
|
-
|
|
33
|
-
expect(result.current).toEqual({ name: "John", age: 30 });
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
describe("selector function", () => {
|
|
38
|
-
it("should support selector function", () => {
|
|
39
|
-
const count$ = atom(5);
|
|
40
|
-
const { result } = renderHook(() =>
|
|
41
|
-
useSelector(({ read }) => read(count$) * 2)
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
expect(result.current).toBe(10);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("should derive from multiple atoms", () => {
|
|
48
|
-
const a$ = atom(2);
|
|
49
|
-
const b$ = atom(3);
|
|
50
|
-
const { result } = renderHook(() =>
|
|
51
|
-
useSelector(({ read }) => read(a$) + read(b$))
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
expect(result.current).toBe(5);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("should update when any source atom changes", async () => {
|
|
58
|
-
const a$ = atom(2);
|
|
59
|
-
const b$ = atom(3);
|
|
60
|
-
const { result } = renderHook(() =>
|
|
61
|
-
useSelector(({ read }) => read(a$) * read(b$))
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
expect(result.current).toBe(6);
|
|
65
|
-
|
|
66
|
-
act(() => {
|
|
67
|
-
a$.set(5);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
await waitFor(() => {
|
|
71
|
-
expect(result.current).toBe(15);
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
describe("conditional dependencies", () => {
|
|
77
|
-
it("should track conditional dependencies", async () => {
|
|
78
|
-
const showDetails$ = atom(false);
|
|
79
|
-
const summary$ = atom("Brief");
|
|
80
|
-
const details$ = atom("Detailed");
|
|
81
|
-
|
|
82
|
-
const { result } = renderHook(() =>
|
|
83
|
-
useSelector(({ read }) =>
|
|
84
|
-
read(showDetails$) ? read(details$) : read(summary$)
|
|
85
|
-
)
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
expect(result.current).toBe("Brief");
|
|
89
|
-
|
|
90
|
-
act(() => {
|
|
91
|
-
showDetails$.set(true);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
await waitFor(() => {
|
|
95
|
-
expect(result.current).toBe("Detailed");
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
describe("equality checks", () => {
|
|
101
|
-
it("should use shallow equality by default", async () => {
|
|
102
|
-
const renderCount = vi.fn();
|
|
103
|
-
const source$ = atom({ a: 1 });
|
|
104
|
-
|
|
105
|
-
const { result } = renderHook(() => {
|
|
106
|
-
renderCount();
|
|
107
|
-
return useSelector(source$);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
expect(result.current).toEqual({ a: 1 });
|
|
111
|
-
|
|
112
|
-
act(() => {
|
|
113
|
-
source$.set({ a: 1 }); // Same content, different reference
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// With shallow equality, same content should not cause re-render
|
|
117
|
-
// (depends on implementation)
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("should support custom equality", async () => {
|
|
121
|
-
const source$ = atom({ id: 1, name: "John" });
|
|
122
|
-
const { result } = renderHook(() =>
|
|
123
|
-
useSelector(source$, (a, b) => a.id === b.id)
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
expect(result.current.name).toBe("John");
|
|
127
|
-
|
|
128
|
-
act(() => {
|
|
129
|
-
source$.set({ id: 1, name: "Jane" }); // Same id
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
// Custom equality by id - should not re-render
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
describe("context utilities", () => {
|
|
137
|
-
it("should support all() in selector", () => {
|
|
138
|
-
const a$ = atom(1);
|
|
139
|
-
const b$ = atom(2);
|
|
140
|
-
const c$ = atom(3);
|
|
141
|
-
|
|
142
|
-
const { result } = renderHook(() =>
|
|
143
|
-
useSelector(({ all }) => {
|
|
144
|
-
const [a, b, c] = all([a$, b$, c$]);
|
|
145
|
-
return a + b + c;
|
|
146
|
-
})
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
expect(result.current).toBe(6);
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
describe("cleanup", () => {
|
|
154
|
-
it("should unsubscribe on unmount", async () => {
|
|
155
|
-
const count$ = atom(0);
|
|
156
|
-
const { unmount } = renderHook(() => useSelector(count$));
|
|
157
|
-
|
|
158
|
-
unmount();
|
|
159
|
-
|
|
160
|
-
// After unmount, setting the atom should not cause issues
|
|
161
|
-
act(() => {
|
|
162
|
-
count$.set(100);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// No error should be thrown
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
// Note: Async/Suspense tests require proper Suspense boundary setup
|
|
170
|
-
// which is more complex to test. The following are placeholder tests.
|
|
171
|
-
|
|
172
|
-
describe("async atoms", () => {
|
|
173
|
-
it("should throw promise for pending atom (Suspense)", () => {
|
|
174
|
-
// When an atom's value is a pending Promise, useSelector should throw
|
|
175
|
-
// the Promise to trigger Suspense. This is hard to test without
|
|
176
|
-
// proper Suspense boundary setup.
|
|
177
|
-
// The hook will throw the Promise which is caught by Suspense
|
|
178
|
-
// Testing this properly requires a Suspense wrapper
|
|
179
|
-
expect(true).toBe(true); // Placeholder
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
});
|
package/src/react/useSelector.ts
DELETED
|
@@ -1,292 +0,0 @@
|
|
|
1
|
-
import { useCallback, useRef, useSyncExternalStore } from "react";
|
|
2
|
-
import { ReactiveSelector, select } from "../core/select";
|
|
3
|
-
import { Atom, Equality } from "../core/types";
|
|
4
|
-
import { resolveEquality } from "../core/equality";
|
|
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
|
-
* useSelector(async ({ read }) => {
|
|
20
|
-
* const data = await fetch('/api');
|
|
21
|
-
* return data;
|
|
22
|
-
* });
|
|
23
|
-
*
|
|
24
|
-
* // ❌ WRONG - Don't return a Promise
|
|
25
|
-
* useSelector(({ read }) => fetch('/api').then(r => r.json()));
|
|
26
|
-
*
|
|
27
|
-
* // ✅ CORRECT - Create async atom and read with read()
|
|
28
|
-
* const data$ = atom(fetch('/api').then(r => r.json()));
|
|
29
|
-
* useSelector(({ read }) => read(data$)); // Suspends until resolved
|
|
30
|
-
* ```
|
|
31
|
-
*
|
|
32
|
-
* ## IMPORTANT: Do NOT Use try/catch - Use safe() Instead
|
|
33
|
-
*
|
|
34
|
-
* **Never wrap `read()` calls in try/catch blocks.** The `read()` function throws
|
|
35
|
-
* Promises when atoms are loading (Suspense pattern). A try/catch will catch
|
|
36
|
-
* these Promises and break the Suspense mechanism.
|
|
37
|
-
*
|
|
38
|
-
* ```tsx
|
|
39
|
-
* // ❌ WRONG - Catches Suspense Promise, breaks loading state
|
|
40
|
-
* const data = useSelector(({ read }) => {
|
|
41
|
-
* try {
|
|
42
|
-
* return read(asyncAtom$);
|
|
43
|
-
* } catch (e) {
|
|
44
|
-
* return null; // This catches BOTH errors AND loading promises!
|
|
45
|
-
* }
|
|
46
|
-
* });
|
|
47
|
-
*
|
|
48
|
-
* // ✅ CORRECT - Use safe() to catch errors but preserve Suspense
|
|
49
|
-
* const result = useSelector(({ read, safe }) => {
|
|
50
|
-
* const [err, data] = safe(() => {
|
|
51
|
-
* const raw = read(asyncAtom$); // Can throw Promise (Suspense)
|
|
52
|
-
* return JSON.parse(raw); // Can throw Error
|
|
53
|
-
* });
|
|
54
|
-
*
|
|
55
|
-
* if (err) return { error: err.message };
|
|
56
|
-
* return { data };
|
|
57
|
-
* });
|
|
58
|
-
* ```
|
|
59
|
-
*
|
|
60
|
-
* The `safe()` utility:
|
|
61
|
-
* - **Catches errors** and returns `[error, undefined]`
|
|
62
|
-
* - **Re-throws Promises** to preserve Suspense behavior
|
|
63
|
-
* - Returns `[undefined, result]` on success
|
|
64
|
-
*
|
|
65
|
-
* ## IMPORTANT: Suspense-Style API
|
|
66
|
-
*
|
|
67
|
-
* This hook uses a **Suspense-style API** for async atoms:
|
|
68
|
-
* - When an atom is **loading**, the getter throws a Promise (suspends)
|
|
69
|
-
* - When an atom has an **error**, the getter throws the error
|
|
70
|
-
* - When an atom is **resolved**, the getter returns the value
|
|
71
|
-
*
|
|
72
|
-
* This means:
|
|
73
|
-
* - **You MUST wrap components with `<Suspense>`** to handle loading states
|
|
74
|
-
* - **You MUST wrap components with `<ErrorBoundary>`** to handle errors
|
|
75
|
-
*
|
|
76
|
-
* ## Alternative: useAsyncState for Non-Suspense
|
|
77
|
-
*
|
|
78
|
-
* If you want to handle loading/error states imperatively without Suspense:
|
|
79
|
-
*
|
|
80
|
-
* ```tsx
|
|
81
|
-
* import { useAsyncState } from 'atomirx/react';
|
|
82
|
-
*
|
|
83
|
-
* function MyComponent() {
|
|
84
|
-
* const state = useAsyncState(myAtom$);
|
|
85
|
-
*
|
|
86
|
-
* if (state.status === "loading") return <Spinner />;
|
|
87
|
-
* if (state.status === "error") return <Error error={state.error} />;
|
|
88
|
-
* return <div>{state.value}</div>;
|
|
89
|
-
* }
|
|
90
|
-
* ```
|
|
91
|
-
*
|
|
92
|
-
* @template T - The type of the selected value
|
|
93
|
-
* @param selectorOrAtom - Atom or context-based selector function (must return sync value)
|
|
94
|
-
* @param equals - Equality function or shorthand. Defaults to "shallow"
|
|
95
|
-
* @returns The selected value (Awaited<T>)
|
|
96
|
-
* @throws Promise when loading (caught by Suspense)
|
|
97
|
-
* @throws Error when failed (caught by ErrorBoundary)
|
|
98
|
-
*
|
|
99
|
-
* @example Single atom (shorthand)
|
|
100
|
-
* ```tsx
|
|
101
|
-
* const count = atom(5);
|
|
102
|
-
*
|
|
103
|
-
* function Counter() {
|
|
104
|
-
* const value = useSelector(count);
|
|
105
|
-
* return <div>{value}</div>;
|
|
106
|
-
* }
|
|
107
|
-
* ```
|
|
108
|
-
*
|
|
109
|
-
* @example With selector
|
|
110
|
-
* ```tsx
|
|
111
|
-
* const count = atom(5);
|
|
112
|
-
*
|
|
113
|
-
* function Counter() {
|
|
114
|
-
* const doubled = useSelector(({ read }) => read(count) * 2);
|
|
115
|
-
* return <div>{doubled}</div>;
|
|
116
|
-
* }
|
|
117
|
-
* ```
|
|
118
|
-
*
|
|
119
|
-
* @example Multiple atoms
|
|
120
|
-
* ```tsx
|
|
121
|
-
* const firstName = atom("John");
|
|
122
|
-
* const lastName = atom("Doe");
|
|
123
|
-
*
|
|
124
|
-
* function FullName() {
|
|
125
|
-
* const fullName = useSelector(({ read }) =>
|
|
126
|
-
* `${read(firstName)} ${read(lastName)}`
|
|
127
|
-
* );
|
|
128
|
-
* return <div>{fullName}</div>;
|
|
129
|
-
* }
|
|
130
|
-
* ```
|
|
131
|
-
*
|
|
132
|
-
* @example Async atom with Suspense
|
|
133
|
-
* ```tsx
|
|
134
|
-
* const userAtom = atom(fetchUser());
|
|
135
|
-
*
|
|
136
|
-
* function UserProfile() {
|
|
137
|
-
* const user = useSelector(({ read }) => read(userAtom));
|
|
138
|
-
* return <div>{user.name}</div>;
|
|
139
|
-
* }
|
|
140
|
-
*
|
|
141
|
-
* // MUST wrap with Suspense and ErrorBoundary
|
|
142
|
-
* function App() {
|
|
143
|
-
* return (
|
|
144
|
-
* <ErrorBoundary fallback={<div>Error!</div>}>
|
|
145
|
-
* <Suspense fallback={<div>Loading...</div>}>
|
|
146
|
-
* <UserProfile />
|
|
147
|
-
* </Suspense>
|
|
148
|
-
* </ErrorBoundary>
|
|
149
|
-
* );
|
|
150
|
-
* }
|
|
151
|
-
* ```
|
|
152
|
-
*
|
|
153
|
-
* @example Using all() for multiple async atoms
|
|
154
|
-
* ```tsx
|
|
155
|
-
* const userAtom = atom(fetchUser());
|
|
156
|
-
* const postsAtom = atom(fetchPosts());
|
|
157
|
-
*
|
|
158
|
-
* function Dashboard() {
|
|
159
|
-
* const data = useSelector(({ all }) => {
|
|
160
|
-
* const [user, posts] = all(userAtom, postsAtom);
|
|
161
|
-
* return { user, posts };
|
|
162
|
-
* });
|
|
163
|
-
*
|
|
164
|
-
* return <DashboardContent user={data.user} posts={data.posts} />;
|
|
165
|
-
* }
|
|
166
|
-
* ```
|
|
167
|
-
*/
|
|
168
|
-
// Overload: Pass atom directly
|
|
169
|
-
export function useSelector<T>(
|
|
170
|
-
atom: Atom<T>,
|
|
171
|
-
equals?: Equality<Awaited<T>>
|
|
172
|
-
): Awaited<T>;
|
|
173
|
-
|
|
174
|
-
// Overload: Context-based selector function
|
|
175
|
-
export function useSelector<T>(
|
|
176
|
-
selector: ReactiveSelector<T>,
|
|
177
|
-
equals?: Equality<T>
|
|
178
|
-
): T;
|
|
179
|
-
|
|
180
|
-
export function useSelector<T>(
|
|
181
|
-
selectorOrAtom: ReactiveSelector<T> | Atom<T>,
|
|
182
|
-
equals?: Equality<T>
|
|
183
|
-
): T {
|
|
184
|
-
// Convert atom shorthand to context selector
|
|
185
|
-
const selector: ReactiveSelector<T> = isAtom(selectorOrAtom)
|
|
186
|
-
? ({ read }) => read(selectorOrAtom as Atom<T>) as T
|
|
187
|
-
: (selectorOrAtom as ReactiveSelector<T>);
|
|
188
|
-
|
|
189
|
-
// Default to shallow equality
|
|
190
|
-
const eq = resolveEquality((equals as Equality<unknown>) ?? "shallow");
|
|
191
|
-
|
|
192
|
-
// Store selector in ref to avoid recreating callbacks
|
|
193
|
-
const selectorRef = useRef(selector);
|
|
194
|
-
const eqRef = useRef(eq);
|
|
195
|
-
|
|
196
|
-
// Update refs on each render
|
|
197
|
-
selectorRef.current = selector;
|
|
198
|
-
eqRef.current = eq;
|
|
199
|
-
|
|
200
|
-
// Track current dependencies and their unsubscribe functions
|
|
201
|
-
const subscriptionsRef = useRef<Map<Atom<unknown>, VoidFunction>>(new Map());
|
|
202
|
-
const dependenciesRef = useRef<Set<Atom<unknown>>>(new Set());
|
|
203
|
-
|
|
204
|
-
// Cache the last snapshot
|
|
205
|
-
const snapshotRef = useRef<{ value: T; initialized: boolean }>({
|
|
206
|
-
value: undefined as T,
|
|
207
|
-
initialized: false,
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Get the current snapshot by running the selector.
|
|
212
|
-
*/
|
|
213
|
-
const getSnapshot = useCallback(() => {
|
|
214
|
-
const result = select(selectorRef.current);
|
|
215
|
-
|
|
216
|
-
// Update dependencies
|
|
217
|
-
dependenciesRef.current = result.dependencies;
|
|
218
|
-
|
|
219
|
-
// Handle Suspense-style states
|
|
220
|
-
if (result.promise !== undefined) {
|
|
221
|
-
// Loading state - throw Promise
|
|
222
|
-
throw result.promise;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (result.error !== undefined) {
|
|
226
|
-
// Error state - throw error
|
|
227
|
-
throw result.error;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Success - check equality and update cache
|
|
231
|
-
const newValue = result.value as T;
|
|
232
|
-
|
|
233
|
-
if (
|
|
234
|
-
!snapshotRef.current.initialized ||
|
|
235
|
-
!eqRef.current(newValue, snapshotRef.current.value)
|
|
236
|
-
) {
|
|
237
|
-
snapshotRef.current = { value: newValue, initialized: true };
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return snapshotRef.current.value;
|
|
241
|
-
}, []);
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Subscribe to atom changes.
|
|
245
|
-
*/
|
|
246
|
-
const subscribe = useCallback((onStoreChange: () => void) => {
|
|
247
|
-
const subscriptions = subscriptionsRef.current;
|
|
248
|
-
|
|
249
|
-
const updateSubscriptions = () => {
|
|
250
|
-
const currentDeps = dependenciesRef.current;
|
|
251
|
-
|
|
252
|
-
// Unsubscribe from atoms no longer dependencies
|
|
253
|
-
for (const [atom, unsubscribe] of subscriptions) {
|
|
254
|
-
if (!currentDeps.has(atom)) {
|
|
255
|
-
unsubscribe();
|
|
256
|
-
subscriptions.delete(atom);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Subscribe to new dependencies
|
|
261
|
-
for (const atom of currentDeps) {
|
|
262
|
-
if (!subscriptions.has(atom)) {
|
|
263
|
-
const unsubscribe = atom.on(() => {
|
|
264
|
-
// Re-run selector to update dependencies
|
|
265
|
-
const result = select(selectorRef.current);
|
|
266
|
-
dependenciesRef.current = result.dependencies;
|
|
267
|
-
|
|
268
|
-
// Update subscriptions if dependencies changed
|
|
269
|
-
updateSubscriptions();
|
|
270
|
-
|
|
271
|
-
// Notify React
|
|
272
|
-
onStoreChange();
|
|
273
|
-
});
|
|
274
|
-
subscriptions.set(atom, unsubscribe);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
// Initial subscription setup
|
|
280
|
-
updateSubscriptions();
|
|
281
|
-
|
|
282
|
-
// Cleanup function
|
|
283
|
-
return () => {
|
|
284
|
-
for (const unsubscribe of subscriptions.values()) {
|
|
285
|
-
unsubscribe();
|
|
286
|
-
}
|
|
287
|
-
subscriptions.clear();
|
|
288
|
-
};
|
|
289
|
-
}, []);
|
|
290
|
-
|
|
291
|
-
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
292
|
-
}
|