rask-ui 0.16.0 → 0.17.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 +1 -1
- package/dist/component.js +3 -3
- package/dist/createContext.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/tests/createEffect.test.js +435 -422
- package/dist/useAsync.js +1 -1
- package/dist/useDerived.d.ts +5 -0
- package/dist/useDerived.d.ts.map +1 -0
- package/dist/useDerived.js +72 -0
- package/dist/useEffect.js +1 -1
- package/dist/useRef.d.ts.map +1 -1
- package/dist/useRef.js +5 -0
- package/dist/useRouter.js +1 -1
- package/dist/useState.d.ts +2 -2
- package/dist/useState.js +3 -3
- package/dist/useView.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ RASK provides a set of reactive hooks for building interactive UIs. These hooks
|
|
|
47
47
|
|
|
48
48
|
- **`useState`** - Create reactive state objects
|
|
49
49
|
- **`useEffect`** - Run side effects when dependencies change
|
|
50
|
-
- **`
|
|
50
|
+
- **`useDerived`** - Derive values from state with automatic caching
|
|
51
51
|
- **`useAsync`** - Manage async operations (fetch, mutations, polling, etc.)
|
|
52
52
|
- **`useRouter`** - Type-safe client-side routing
|
|
53
53
|
- **`createContext`** / **`useContext`** - Share data through the component tree
|
package/dist/component.js
CHANGED
|
@@ -28,13 +28,13 @@ export function getCurrentComponent() {
|
|
|
28
28
|
}
|
|
29
29
|
export function useMountEffect(cb) {
|
|
30
30
|
if (!currentComponent) {
|
|
31
|
-
throw new Error("Only use
|
|
31
|
+
throw new Error("Only use useMountEffect in component setup");
|
|
32
32
|
}
|
|
33
33
|
currentComponent.onMounts.push(cb);
|
|
34
34
|
}
|
|
35
35
|
export function useCleanup(cb) {
|
|
36
36
|
if (!currentComponent || currentComponent.isRendering) {
|
|
37
|
-
throw new Error("Only use
|
|
37
|
+
throw new Error("Only use useCleanup in component setup");
|
|
38
38
|
}
|
|
39
39
|
currentComponent.onCleanups.push(cb);
|
|
40
40
|
}
|
|
@@ -91,7 +91,7 @@ export class RaskStatefulComponent extends Component {
|
|
|
91
91
|
enumerable: true,
|
|
92
92
|
get() {
|
|
93
93
|
const observer = getCurrentObserver();
|
|
94
|
-
if (
|
|
94
|
+
if (observer) {
|
|
95
95
|
// Lazy create signal only when accessed in reactive context
|
|
96
96
|
let signal = signals.get(prop);
|
|
97
97
|
if (!signal) {
|
package/dist/createContext.js
CHANGED
|
@@ -29,7 +29,7 @@ export function createContext() {
|
|
|
29
29
|
export function useContext(context) {
|
|
30
30
|
let currentComponent = getCurrentComponent();
|
|
31
31
|
if (!currentComponent) {
|
|
32
|
-
throw new Error("
|
|
32
|
+
throw new Error("Only use useContext in component setup");
|
|
33
33
|
}
|
|
34
34
|
if (typeof currentComponent.context.getContext !== "function") {
|
|
35
35
|
throw new Error("There is no parent context");
|
|
@@ -44,7 +44,7 @@ export function useInjectContext(context) {
|
|
|
44
44
|
return (value) => {
|
|
45
45
|
const currentComponent = getCurrentComponent();
|
|
46
46
|
if (!currentComponent) {
|
|
47
|
-
throw new Error("
|
|
47
|
+
throw new Error("Only use useInjectContext in component setup");
|
|
48
48
|
}
|
|
49
49
|
currentComponent.contexts.set(context, value);
|
|
50
50
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export { ErrorBoundary } from "./error";
|
|
|
7
7
|
export { useRef } from "./useRef";
|
|
8
8
|
export { useView } from "./useView";
|
|
9
9
|
export { useEffect } from "./useEffect";
|
|
10
|
-
export {
|
|
10
|
+
export { useDerived, Derived } from "./useDerived";
|
|
11
11
|
export { syncBatch } from "./batch";
|
|
12
12
|
export { inspect } from "./inspect";
|
|
13
13
|
export { Router, useRouter } from "./useRouter";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EACL,UAAU,EACV,cAAc,EACd,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAC9E,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EACL,UAAU,EACV,cAAc,EACd,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAC9E,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAGhD,OAAO,EACL,WAAW,EACX,oBAAoB,EACpB,cAAc,EACd,eAAe,EACf,cAAc,EACd,SAAS,GACV,MAAM,SAAS,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ export { ErrorBoundary } from "./error";
|
|
|
7
7
|
export { useRef } from "./useRef";
|
|
8
8
|
export { useView } from "./useView";
|
|
9
9
|
export { useEffect } from "./useEffect";
|
|
10
|
-
export {
|
|
10
|
+
export { useDerived } from "./useDerived";
|
|
11
11
|
export { syncBatch } from "./batch";
|
|
12
12
|
export { inspect } from "./inspect";
|
|
13
13
|
export { useRouter } from "./useRouter";
|
|
@@ -4,451 +4,464 @@ import { createEffect } from "../createEffect";
|
|
|
4
4
|
import { createState } from "../createState";
|
|
5
5
|
import { render } from "../index";
|
|
6
6
|
describe("createEffect", () => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
it("should run immediately on creation", () => {
|
|
8
|
+
const effectFn = vi.fn();
|
|
9
|
+
createEffect(effectFn);
|
|
10
|
+
expect(effectFn).toHaveBeenCalledTimes(1);
|
|
11
|
+
});
|
|
12
|
+
it("should track reactive dependencies", async () => {
|
|
13
|
+
const state = createState({ count: 0 });
|
|
14
|
+
const effectFn = vi.fn(() => {
|
|
15
|
+
state.count; // Access to track
|
|
11
16
|
});
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
createEffect(effectFn);
|
|
18
|
+
expect(effectFn).toHaveBeenCalledTimes(1);
|
|
19
|
+
state.count = 1;
|
|
20
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
21
|
+
expect(effectFn).toHaveBeenCalledTimes(2);
|
|
22
|
+
});
|
|
23
|
+
it("should re-run when dependencies change", async () => {
|
|
24
|
+
const state = createState({ count: 0 });
|
|
25
|
+
const results = [];
|
|
26
|
+
createEffect(() => {
|
|
27
|
+
results.push(state.count);
|
|
22
28
|
});
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
expect(results).toEqual([0]);
|
|
30
|
+
state.count = 1;
|
|
31
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
32
|
+
expect(results).toEqual([0, 1]);
|
|
33
|
+
state.count = 2;
|
|
34
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
35
|
+
expect(results).toEqual([0, 1, 2]);
|
|
36
|
+
});
|
|
37
|
+
it("should run on microtask, not synchronously", () => {
|
|
38
|
+
const state = createState({ count: 0 });
|
|
39
|
+
const results = [];
|
|
40
|
+
createEffect(() => {
|
|
41
|
+
results.push(state.count);
|
|
36
42
|
});
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
expect(results).toEqual([0]); // Initial run is synchronous
|
|
44
|
+
state.count = 1;
|
|
45
|
+
// Should not have run yet (microtask not flushed)
|
|
46
|
+
expect(results).toEqual([0]);
|
|
47
|
+
});
|
|
48
|
+
it("should handle multiple effects on same state", async () => {
|
|
49
|
+
const state = createState({ count: 0 });
|
|
50
|
+
const results1 = [];
|
|
51
|
+
const results2 = [];
|
|
52
|
+
createEffect(() => {
|
|
53
|
+
results1.push(state.count);
|
|
47
54
|
});
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const results1 = [];
|
|
51
|
-
const results2 = [];
|
|
52
|
-
createEffect(() => {
|
|
53
|
-
results1.push(state.count);
|
|
54
|
-
});
|
|
55
|
-
createEffect(() => {
|
|
56
|
-
results2.push(state.count * 2);
|
|
57
|
-
});
|
|
58
|
-
expect(results1).toEqual([0]);
|
|
59
|
-
expect(results2).toEqual([0]);
|
|
60
|
-
state.count = 5;
|
|
61
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
62
|
-
expect(results1).toEqual([0, 5]);
|
|
63
|
-
expect(results2).toEqual([0, 10]);
|
|
55
|
+
createEffect(() => {
|
|
56
|
+
results2.push(state.count * 2);
|
|
64
57
|
});
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
// Change 'a' (tracked)
|
|
77
|
-
state.a = 10;
|
|
78
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
79
|
-
expect(effectFn).toHaveBeenCalledTimes(2); // Should re-run
|
|
58
|
+
expect(results1).toEqual([0]);
|
|
59
|
+
expect(results2).toEqual([0]);
|
|
60
|
+
state.count = 5;
|
|
61
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
62
|
+
expect(results1).toEqual([0, 5]);
|
|
63
|
+
expect(results2).toEqual([0, 10]);
|
|
64
|
+
});
|
|
65
|
+
it("should only track dependencies accessed during execution", async () => {
|
|
66
|
+
const state = createState({ a: 1, b: 2 });
|
|
67
|
+
const effectFn = vi.fn(() => {
|
|
68
|
+
state.a; // Only track 'a'
|
|
80
69
|
});
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
state.b
|
|
99
|
-
|
|
100
|
-
expect(effectFn).toHaveBeenCalledTimes(2); // No change
|
|
101
|
-
// Switch to using 'b'
|
|
102
|
-
state.useA = false;
|
|
103
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
104
|
-
expect(effectFn).toHaveBeenCalledTimes(3);
|
|
105
|
-
// Change 'a' (no longer tracked)
|
|
106
|
-
state.a = 100;
|
|
107
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
108
|
-
expect(effectFn).toHaveBeenCalledTimes(3); // No change
|
|
109
|
-
// Change 'b' (now tracked)
|
|
110
|
-
state.b = 200;
|
|
111
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
112
|
-
expect(effectFn).toHaveBeenCalledTimes(4);
|
|
70
|
+
createEffect(effectFn);
|
|
71
|
+
expect(effectFn).toHaveBeenCalledTimes(1);
|
|
72
|
+
// Change 'b' (not tracked)
|
|
73
|
+
state.b = 100;
|
|
74
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
75
|
+
expect(effectFn).toHaveBeenCalledTimes(1); // Should not re-run
|
|
76
|
+
// Change 'a' (tracked)
|
|
77
|
+
state.a = 10;
|
|
78
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
79
|
+
expect(effectFn).toHaveBeenCalledTimes(2); // Should re-run
|
|
80
|
+
});
|
|
81
|
+
it("should re-track dependencies on each run", async () => {
|
|
82
|
+
const state = createState({ useA: true, a: 1, b: 2 });
|
|
83
|
+
const effectFn = vi.fn(() => {
|
|
84
|
+
if (state.useA) {
|
|
85
|
+
state.a;
|
|
86
|
+
} else {
|
|
87
|
+
state.b;
|
|
88
|
+
}
|
|
113
89
|
});
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
90
|
+
createEffect(effectFn);
|
|
91
|
+
expect(effectFn).toHaveBeenCalledTimes(1);
|
|
92
|
+
// Change 'a' (currently tracked)
|
|
93
|
+
state.a = 10;
|
|
94
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
95
|
+
expect(effectFn).toHaveBeenCalledTimes(2);
|
|
96
|
+
// Change 'b' (not tracked)
|
|
97
|
+
state.b = 20;
|
|
98
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
99
|
+
expect(effectFn).toHaveBeenCalledTimes(2); // No change
|
|
100
|
+
// Switch to using 'b'
|
|
101
|
+
state.useA = false;
|
|
102
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
103
|
+
expect(effectFn).toHaveBeenCalledTimes(3);
|
|
104
|
+
// Change 'a' (no longer tracked)
|
|
105
|
+
state.a = 100;
|
|
106
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
107
|
+
expect(effectFn).toHaveBeenCalledTimes(3); // No change
|
|
108
|
+
// Change 'b' (now tracked)
|
|
109
|
+
state.b = 200;
|
|
110
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
111
|
+
expect(effectFn).toHaveBeenCalledTimes(4);
|
|
112
|
+
});
|
|
113
|
+
it("should handle effects that modify state", async () => {
|
|
114
|
+
const state = createState({ input: 1, output: 0 });
|
|
115
|
+
createEffect(() => {
|
|
116
|
+
state.output = state.input * 2;
|
|
123
117
|
});
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
expect(results).toEqual(["Alice"]);
|
|
137
|
-
state.user.profile.name = "Bob";
|
|
138
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
139
|
-
expect(results).toEqual(["Alice", "Bob"]);
|
|
118
|
+
expect(state.output).toBe(2);
|
|
119
|
+
state.input = 5;
|
|
120
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
121
|
+
expect(state.output).toBe(10);
|
|
122
|
+
});
|
|
123
|
+
it("should handle nested state access", async () => {
|
|
124
|
+
const state = createState({
|
|
125
|
+
user: {
|
|
126
|
+
profile: {
|
|
127
|
+
name: "Alice",
|
|
128
|
+
},
|
|
129
|
+
},
|
|
140
130
|
});
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
createEffect(() => {
|
|
145
|
-
results.push(state.items.length);
|
|
146
|
-
});
|
|
147
|
-
expect(results).toEqual([3]);
|
|
148
|
-
state.items.push(4);
|
|
149
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
150
|
-
expect(results).toEqual([3, 4]);
|
|
151
|
-
state.items.pop();
|
|
152
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
153
|
-
expect(results).toEqual([3, 4, 3]);
|
|
131
|
+
const results = [];
|
|
132
|
+
createEffect(() => {
|
|
133
|
+
results.push(state.user.profile.name);
|
|
154
134
|
});
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
expect(results).toEqual([6, 10]);
|
|
166
|
-
state.items[0] = 10;
|
|
167
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
168
|
-
expect(results).toEqual([6, 10, 19]);
|
|
135
|
+
expect(results).toEqual(["Alice"]);
|
|
136
|
+
state.user.profile.name = "Bob";
|
|
137
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
138
|
+
expect(results).toEqual(["Alice", "Bob"]);
|
|
139
|
+
});
|
|
140
|
+
it("should handle array access", async () => {
|
|
141
|
+
const state = createState({ items: [1, 2, 3] });
|
|
142
|
+
const results = [];
|
|
143
|
+
createEffect(() => {
|
|
144
|
+
results.push(state.items.length);
|
|
169
145
|
});
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
expect(effectFn).toHaveBeenCalledTimes(2);
|
|
146
|
+
expect(results).toEqual([3]);
|
|
147
|
+
state.items.push(4);
|
|
148
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
149
|
+
expect(results).toEqual([3, 4]);
|
|
150
|
+
state.items.pop();
|
|
151
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
152
|
+
expect(results).toEqual([3, 4, 3]);
|
|
153
|
+
});
|
|
154
|
+
it("should handle effects accessing array elements", async () => {
|
|
155
|
+
const state = createState({ items: [1, 2, 3] });
|
|
156
|
+
const results = [];
|
|
157
|
+
createEffect(() => {
|
|
158
|
+
const sum = state.items.reduce((acc, val) => acc + val, 0);
|
|
159
|
+
results.push(sum);
|
|
185
160
|
});
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
161
|
+
expect(results).toEqual([6]);
|
|
162
|
+
state.items.push(4);
|
|
163
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
164
|
+
expect(results).toEqual([6, 10]);
|
|
165
|
+
state.items[0] = 10;
|
|
166
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
167
|
+
expect(results).toEqual([6, 10, 19]);
|
|
168
|
+
});
|
|
169
|
+
it("should batch multiple state changes before re-running", async () => {
|
|
170
|
+
const state = createState({ a: 1, b: 2 });
|
|
171
|
+
const effectFn = vi.fn(() => {
|
|
172
|
+
state.a;
|
|
173
|
+
state.b;
|
|
196
174
|
});
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
213
|
-
expect(effectFn).toHaveBeenCalledTimes(3);
|
|
214
|
-
// Change value (not tracked because enabled is false)
|
|
215
|
-
state.value = 20;
|
|
216
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
217
|
-
expect(effectFn).toHaveBeenCalledTimes(3); // No change
|
|
175
|
+
createEffect(effectFn);
|
|
176
|
+
expect(effectFn).toHaveBeenCalledTimes(1);
|
|
177
|
+
// Multiple changes in same turn
|
|
178
|
+
state.a = 10;
|
|
179
|
+
state.b = 20;
|
|
180
|
+
state.a = 15;
|
|
181
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
182
|
+
// Should only run once for all changes
|
|
183
|
+
expect(effectFn).toHaveBeenCalledTimes(2);
|
|
184
|
+
});
|
|
185
|
+
it("should handle effects with no dependencies", async () => {
|
|
186
|
+
let runCount = 0;
|
|
187
|
+
createEffect(() => {
|
|
188
|
+
runCount++;
|
|
189
|
+
// No reactive state accessed
|
|
218
190
|
});
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
state.multiplier = 3;
|
|
231
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
232
|
-
expect(results).toEqual([12, 18]); // (1+2+3) * 3
|
|
233
|
-
state.values.push(4);
|
|
234
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
235
|
-
expect(results).toEqual([12, 18, 30]); // (1+2+3+4) * 3
|
|
191
|
+
expect(runCount).toBe(1);
|
|
192
|
+
// Wait to ensure it doesn't run again
|
|
193
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
194
|
+
expect(runCount).toBe(1);
|
|
195
|
+
});
|
|
196
|
+
it("should handle effects that conditionally access state", async () => {
|
|
197
|
+
const state = createState({ enabled: true, value: 5 });
|
|
198
|
+
const effectFn = vi.fn(() => {
|
|
199
|
+
if (state.enabled) {
|
|
200
|
+
state.value;
|
|
201
|
+
}
|
|
236
202
|
});
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
]);
|
|
203
|
+
createEffect(effectFn);
|
|
204
|
+
expect(effectFn).toHaveBeenCalledTimes(1);
|
|
205
|
+
// Change value (tracked because enabled is true)
|
|
206
|
+
state.value = 10;
|
|
207
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
208
|
+
expect(effectFn).toHaveBeenCalledTimes(2);
|
|
209
|
+
// Disable
|
|
210
|
+
state.enabled = false;
|
|
211
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
212
|
+
expect(effectFn).toHaveBeenCalledTimes(3);
|
|
213
|
+
// Change value (not tracked because enabled is false)
|
|
214
|
+
state.value = 20;
|
|
215
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
216
|
+
expect(effectFn).toHaveBeenCalledTimes(3); // No change
|
|
217
|
+
});
|
|
218
|
+
it("should handle complex dependency graphs", async () => {
|
|
219
|
+
const state = createState({
|
|
220
|
+
multiplier: 2,
|
|
221
|
+
values: [1, 2, 3],
|
|
257
222
|
});
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
});
|
|
263
|
-
createEffect(effectFn);
|
|
264
|
-
expect(effectFn).toHaveBeenCalledTimes(1);
|
|
265
|
-
// Rapid changes
|
|
266
|
-
for (let i = 1; i <= 10; i++) {
|
|
267
|
-
state.count = i;
|
|
268
|
-
}
|
|
269
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
270
|
-
// Should batch all changes into one effect run
|
|
271
|
-
expect(effectFn).toHaveBeenCalledTimes(2);
|
|
223
|
+
const results = [];
|
|
224
|
+
createEffect(() => {
|
|
225
|
+
const sum = state.values.reduce((acc, val) => acc + val, 0);
|
|
226
|
+
results.push(sum * state.multiplier);
|
|
272
227
|
});
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
228
|
+
expect(results).toEqual([12]); // (1+2+3) * 2
|
|
229
|
+
state.multiplier = 3;
|
|
230
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
231
|
+
expect(results).toEqual([12, 18]); // (1+2+3) * 3
|
|
232
|
+
state.values.push(4);
|
|
233
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
234
|
+
expect(results).toEqual([12, 18, 30]); // (1+2+3+4) * 3
|
|
235
|
+
});
|
|
236
|
+
it("should handle effects that run other synchronous code", async () => {
|
|
237
|
+
const state = createState({ count: 0 });
|
|
238
|
+
const sideEffects = [];
|
|
239
|
+
createEffect(() => {
|
|
240
|
+
sideEffects.push("effect-start");
|
|
241
|
+
const value = state.count;
|
|
242
|
+
sideEffects.push(`value-${value}`);
|
|
243
|
+
sideEffects.push("effect-end");
|
|
285
244
|
});
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
expect(disposeCalls).toEqual([1]);
|
|
303
|
-
expect(effectCalls).toEqual([0, 1]);
|
|
304
|
-
state.count = 2;
|
|
305
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
306
|
-
expect(disposeCalls).toEqual([1, 2]);
|
|
307
|
-
expect(effectCalls).toEqual([0, 1, 2]);
|
|
245
|
+
expect(sideEffects).toEqual(["effect-start", "value-0", "effect-end"]);
|
|
246
|
+
state.count = 1;
|
|
247
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
248
|
+
expect(sideEffects).toEqual([
|
|
249
|
+
"effect-start",
|
|
250
|
+
"value-0",
|
|
251
|
+
"effect-end",
|
|
252
|
+
"effect-start",
|
|
253
|
+
"value-1",
|
|
254
|
+
"effect-end",
|
|
255
|
+
]);
|
|
256
|
+
});
|
|
257
|
+
it("should handle rapid state changes", async () => {
|
|
258
|
+
const state = createState({ count: 0 });
|
|
259
|
+
const effectFn = vi.fn(() => {
|
|
260
|
+
state.count;
|
|
308
261
|
});
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
"unsubscribe:/api/data",
|
|
325
|
-
"subscribe:/api/users",
|
|
326
|
-
]);
|
|
327
|
-
state.url = "/api/posts";
|
|
328
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
329
|
-
expect(subscriptions).toEqual([
|
|
330
|
-
"subscribe:/api/data",
|
|
331
|
-
"unsubscribe:/api/data",
|
|
332
|
-
"subscribe:/api/users",
|
|
333
|
-
"unsubscribe:/api/users",
|
|
334
|
-
"subscribe:/api/posts",
|
|
335
|
-
]);
|
|
262
|
+
createEffect(effectFn);
|
|
263
|
+
expect(effectFn).toHaveBeenCalledTimes(1);
|
|
264
|
+
// Rapid changes
|
|
265
|
+
for (let i = 1; i <= 10; i++) {
|
|
266
|
+
state.count = i;
|
|
267
|
+
}
|
|
268
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
269
|
+
// Should batch all changes into one effect run
|
|
270
|
+
expect(effectFn).toHaveBeenCalledTimes(2);
|
|
271
|
+
});
|
|
272
|
+
it("should access latest state values when effect runs", async () => {
|
|
273
|
+
const state = createState({ count: 0 });
|
|
274
|
+
const results = [];
|
|
275
|
+
createEffect(() => {
|
|
276
|
+
results.push(state.count);
|
|
336
277
|
});
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
278
|
+
state.count = 1;
|
|
279
|
+
state.count = 2;
|
|
280
|
+
state.count = 3;
|
|
281
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
282
|
+
// Should see latest value when effect runs
|
|
283
|
+
expect(results).toEqual([0, 3]);
|
|
284
|
+
});
|
|
285
|
+
it("should call dispose function before re-executing", async () => {
|
|
286
|
+
const state = createState({ count: 0 });
|
|
287
|
+
const disposeCalls = [];
|
|
288
|
+
const effectCalls = [];
|
|
289
|
+
createEffect(() => {
|
|
290
|
+
effectCalls.push(state.count);
|
|
291
|
+
return () => {
|
|
292
|
+
// Dispose sees the current state at the time it's called
|
|
293
|
+
disposeCalls.push(state.count);
|
|
294
|
+
};
|
|
351
295
|
});
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
296
|
+
expect(effectCalls).toEqual([0]);
|
|
297
|
+
expect(disposeCalls).toEqual([]);
|
|
298
|
+
state.count = 1;
|
|
299
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
300
|
+
// Dispose is called after state change, before effect re-runs
|
|
301
|
+
expect(disposeCalls).toEqual([1]);
|
|
302
|
+
expect(effectCalls).toEqual([0, 1]);
|
|
303
|
+
state.count = 2;
|
|
304
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
305
|
+
expect(disposeCalls).toEqual([1, 2]);
|
|
306
|
+
expect(effectCalls).toEqual([0, 1, 2]);
|
|
307
|
+
});
|
|
308
|
+
it("should handle dispose function with cleanup logic", async () => {
|
|
309
|
+
const state = createState({ url: "/api/data" });
|
|
310
|
+
const subscriptions = [];
|
|
311
|
+
createEffect(() => {
|
|
312
|
+
const currentUrl = state.url;
|
|
313
|
+
subscriptions.push(`subscribe:${currentUrl}`);
|
|
314
|
+
return () => {
|
|
315
|
+
subscriptions.push(`unsubscribe:${currentUrl}`);
|
|
316
|
+
};
|
|
371
317
|
});
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
318
|
+
expect(subscriptions).toEqual(["subscribe:/api/data"]);
|
|
319
|
+
state.url = "/api/users";
|
|
320
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
321
|
+
expect(subscriptions).toEqual([
|
|
322
|
+
"subscribe:/api/data",
|
|
323
|
+
"unsubscribe:/api/data",
|
|
324
|
+
"subscribe:/api/users",
|
|
325
|
+
]);
|
|
326
|
+
state.url = "/api/posts";
|
|
327
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
328
|
+
expect(subscriptions).toEqual([
|
|
329
|
+
"subscribe:/api/data",
|
|
330
|
+
"unsubscribe:/api/data",
|
|
331
|
+
"subscribe:/api/users",
|
|
332
|
+
"unsubscribe:/api/users",
|
|
333
|
+
"subscribe:/api/posts",
|
|
334
|
+
]);
|
|
335
|
+
});
|
|
336
|
+
it("should handle effects without dispose function", async () => {
|
|
337
|
+
const state = createState({ count: 0 });
|
|
338
|
+
const effectCalls = [];
|
|
339
|
+
createEffect(() => {
|
|
340
|
+
effectCalls.push(state.count);
|
|
341
|
+
// No dispose function returned
|
|
390
342
|
});
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
343
|
+
expect(effectCalls).toEqual([0]);
|
|
344
|
+
state.count = 1;
|
|
345
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
346
|
+
expect(effectCalls).toEqual([0, 1]);
|
|
347
|
+
state.count = 2;
|
|
348
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
349
|
+
expect(effectCalls).toEqual([0, 1, 2]);
|
|
350
|
+
});
|
|
351
|
+
it("should handle dispose function that throws error", async () => {
|
|
352
|
+
const state = createState({ count: 0 });
|
|
353
|
+
const effectCalls = [];
|
|
354
|
+
const consoleErrorSpy = vi
|
|
355
|
+
.spyOn(console, "error")
|
|
356
|
+
.mockImplementation(() => {});
|
|
357
|
+
createEffect(() => {
|
|
358
|
+
effectCalls.push(state.count);
|
|
359
|
+
return () => {
|
|
360
|
+
throw new Error("Dispose error");
|
|
361
|
+
};
|
|
362
|
+
});
|
|
363
|
+
expect(effectCalls).toEqual([0]);
|
|
364
|
+
state.count = 1;
|
|
365
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
366
|
+
// Effect should still have run despite dispose throwing
|
|
367
|
+
expect(effectCalls).toEqual([0, 1]);
|
|
368
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
369
|
+
"Error in effect dispose function:",
|
|
370
|
+
expect.any(Error)
|
|
371
|
+
);
|
|
372
|
+
consoleErrorSpy.mockRestore();
|
|
373
|
+
});
|
|
374
|
+
it("should call dispose with latest closure values", async () => {
|
|
375
|
+
const state = createState({ count: 0 });
|
|
376
|
+
const disposeValues = [];
|
|
377
|
+
createEffect(() => {
|
|
378
|
+
const capturedCount = state.count;
|
|
379
|
+
return () => {
|
|
380
|
+
disposeValues.push(capturedCount);
|
|
381
|
+
};
|
|
411
382
|
});
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
const state = createState({ count: 1 });
|
|
427
|
-
return () => {
|
|
428
|
-
renderLog.push(`parent-render:${state.count}`);
|
|
429
|
-
return (_jsxs("div", { children: [_jsx(Child, { value: state.count }), _jsx("button", { onClick: () => {
|
|
430
|
-
state.count = 2;
|
|
431
|
-
}, children: "Increment" })] }));
|
|
432
|
-
};
|
|
433
|
-
}
|
|
434
|
-
const container = document.createElement("div");
|
|
435
|
-
document.body.appendChild(container);
|
|
436
|
-
render(_jsx(Parent, {}), container);
|
|
437
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
438
|
-
// Initial render: parent renders, then child renders with effect-updated state
|
|
439
|
-
expect(renderLog).toEqual(["parent-render:1", "render:2"]);
|
|
440
|
-
const childDiv = container.querySelector(".child-component");
|
|
441
|
-
expect(childDiv?.textContent).toBe("2");
|
|
442
|
-
// Clear log and trigger update
|
|
443
|
-
renderLog.length = 0;
|
|
444
|
-
const button = container.querySelector("button");
|
|
445
|
-
button.click();
|
|
446
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
447
|
-
console.log(renderLog);
|
|
448
|
-
// After prop update: parent renders, child's effect runs synchronously updating state,
|
|
449
|
-
// then child renders once with the updated state
|
|
450
|
-
expect(renderLog).toEqual(["parent-render:2", "render:4"]);
|
|
451
|
-
expect(childDiv?.textContent).toBe("4");
|
|
452
|
-
document.body.removeChild(container);
|
|
383
|
+
state.count = 1;
|
|
384
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
385
|
+
expect(disposeValues).toEqual([0]);
|
|
386
|
+
state.count = 5;
|
|
387
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
388
|
+
expect(disposeValues).toEqual([0, 1]);
|
|
389
|
+
state.count = 10;
|
|
390
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
391
|
+
expect(disposeValues).toEqual([0, 1, 5]);
|
|
392
|
+
});
|
|
393
|
+
it("should handle rapid state changes with dispose", async () => {
|
|
394
|
+
const state = createState({ count: 0 });
|
|
395
|
+
const effectFn = vi.fn(() => {
|
|
396
|
+
state.count;
|
|
453
397
|
});
|
|
398
|
+
const disposeFn = vi.fn();
|
|
399
|
+
createEffect(() => {
|
|
400
|
+
effectFn();
|
|
401
|
+
return disposeFn;
|
|
402
|
+
});
|
|
403
|
+
expect(effectFn).toHaveBeenCalledTimes(1);
|
|
404
|
+
expect(disposeFn).toHaveBeenCalledTimes(0);
|
|
405
|
+
// Rapid changes should batch
|
|
406
|
+
state.count = 1;
|
|
407
|
+
state.count = 2;
|
|
408
|
+
state.count = 3;
|
|
409
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
410
|
+
// Effect and dispose should each be called once more
|
|
411
|
+
expect(effectFn).toHaveBeenCalledTimes(2);
|
|
412
|
+
expect(disposeFn).toHaveBeenCalledTimes(1);
|
|
413
|
+
});
|
|
414
|
+
it("should run effects synchronously before render when props change", async () => {
|
|
415
|
+
const renderLog = [];
|
|
416
|
+
function Child(props) {
|
|
417
|
+
const state = createState({ internalValue: 0 });
|
|
418
|
+
createEffect(() => {
|
|
419
|
+
// Update internal state based on prop
|
|
420
|
+
state.internalValue = props.value * 2;
|
|
421
|
+
});
|
|
422
|
+
return () => {
|
|
423
|
+
renderLog.push(`render:${state.internalValue}`);
|
|
424
|
+
return _jsx("div", {
|
|
425
|
+
class: "child-component",
|
|
426
|
+
children: state.internalValue,
|
|
427
|
+
});
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
function Parent() {
|
|
431
|
+
const state = createState({ count: 1 });
|
|
432
|
+
return () => {
|
|
433
|
+
renderLog.push(`parent-render:${state.count}`);
|
|
434
|
+
return _jsxs("div", {
|
|
435
|
+
children: [
|
|
436
|
+
_jsx(Child, { value: state.count }),
|
|
437
|
+
_jsx("button", {
|
|
438
|
+
onClick: () => {
|
|
439
|
+
state.count = 2;
|
|
440
|
+
},
|
|
441
|
+
children: "Increment",
|
|
442
|
+
}),
|
|
443
|
+
],
|
|
444
|
+
});
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
const container = document.createElement("div");
|
|
448
|
+
document.body.appendChild(container);
|
|
449
|
+
render(_jsx(Parent, {}), container);
|
|
450
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
451
|
+
// Initial render: parent renders, then child renders with effect-updated state
|
|
452
|
+
expect(renderLog).toEqual(["parent-render:1", "render:2"]);
|
|
453
|
+
const childDiv = container.querySelector(".child-component");
|
|
454
|
+
expect(childDiv?.textContent).toBe("2");
|
|
455
|
+
// Clear log and trigger update
|
|
456
|
+
renderLog.length = 0;
|
|
457
|
+
const button = container.querySelector("button");
|
|
458
|
+
button.click();
|
|
459
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
460
|
+
|
|
461
|
+
// After prop update: parent renders, child's effect runs synchronously updating state,
|
|
462
|
+
// then child renders once with the updated state
|
|
463
|
+
expect(renderLog).toEqual(["parent-render:2", "render:4"]);
|
|
464
|
+
expect(childDiv?.textContent).toBe("4");
|
|
465
|
+
document.body.removeChild(container);
|
|
466
|
+
});
|
|
454
467
|
});
|
package/dist/useAsync.js
CHANGED
|
@@ -3,7 +3,7 @@ import { assignState, useState } from "./useState";
|
|
|
3
3
|
export function useAsync(...args) {
|
|
4
4
|
const currentComponent = getCurrentComponent();
|
|
5
5
|
if (!currentComponent || currentComponent.isRendering) {
|
|
6
|
-
throw new Error("Only use
|
|
6
|
+
throw new Error("Only use useTask in component setup");
|
|
7
7
|
}
|
|
8
8
|
const value = args.length === 2 ? args[0] : null;
|
|
9
9
|
const fn = args.length === 2 ? args[1] : args[0];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useDerived.d.ts","sourceRoot":"","sources":["../src/useDerived.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,OAAO,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC,IAAI;KACxD,CAAC,IAAI,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CACjC,CAAC;AAEF,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC,EAC5D,QAAQ,EAAE,CAAC,GACV,OAAO,CAAC,CAAC,CAAC,CAgFZ"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { getCurrentComponent, useCleanup } from "./component";
|
|
2
|
+
import { INSPECT_MARKER, INSPECTOR_ENABLED } from "./inspect";
|
|
3
|
+
import { getCurrentObserver, Observer, Signal } from "./observation";
|
|
4
|
+
export function useDerived(computed) {
|
|
5
|
+
const currentComponent = getCurrentComponent();
|
|
6
|
+
if (!currentComponent || currentComponent.isRendering) {
|
|
7
|
+
throw new Error("Only use useDerived in component setup");
|
|
8
|
+
}
|
|
9
|
+
const proxy = {};
|
|
10
|
+
let notifyInspectorRef = {};
|
|
11
|
+
for (const prop in computed) {
|
|
12
|
+
let isDirty = true;
|
|
13
|
+
let value;
|
|
14
|
+
const signal = new Signal();
|
|
15
|
+
const computedObserver = new Observer(() => {
|
|
16
|
+
isDirty = true;
|
|
17
|
+
signal.notify();
|
|
18
|
+
if (INSPECTOR_ENABLED) {
|
|
19
|
+
notifyInspectorRef.current?.notify({
|
|
20
|
+
type: "computed",
|
|
21
|
+
path: notifyInspectorRef.current.path.concat(prop),
|
|
22
|
+
isDirty: true,
|
|
23
|
+
value,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
useCleanup(() => computedObserver.dispose());
|
|
28
|
+
Object.defineProperty(proxy, prop, {
|
|
29
|
+
enumerable: true,
|
|
30
|
+
configurable: true,
|
|
31
|
+
get() {
|
|
32
|
+
const currentObserver = getCurrentObserver();
|
|
33
|
+
if (currentObserver) {
|
|
34
|
+
currentObserver.subscribeSignal(signal);
|
|
35
|
+
}
|
|
36
|
+
if (isDirty) {
|
|
37
|
+
const stopObserving = computedObserver.observe();
|
|
38
|
+
value = computed[prop]();
|
|
39
|
+
stopObserving();
|
|
40
|
+
isDirty = false;
|
|
41
|
+
if (INSPECTOR_ENABLED) {
|
|
42
|
+
notifyInspectorRef.current?.notify({
|
|
43
|
+
type: "computed",
|
|
44
|
+
path: notifyInspectorRef.current.path.concat(prop),
|
|
45
|
+
isDirty: false,
|
|
46
|
+
value,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (INSPECTOR_ENABLED) {
|
|
56
|
+
Object.defineProperty(proxy, INSPECT_MARKER, {
|
|
57
|
+
enumerable: false,
|
|
58
|
+
configurable: false,
|
|
59
|
+
get() {
|
|
60
|
+
return !notifyInspectorRef.current;
|
|
61
|
+
},
|
|
62
|
+
set: (value) => {
|
|
63
|
+
Object.defineProperty(notifyInspectorRef, "current", {
|
|
64
|
+
get() {
|
|
65
|
+
return value.current;
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return proxy;
|
|
72
|
+
}
|
package/dist/useEffect.js
CHANGED
|
@@ -4,7 +4,7 @@ import { Observer } from "./observation";
|
|
|
4
4
|
export function useEffect(cb) {
|
|
5
5
|
const component = getCurrentComponent();
|
|
6
6
|
if (!component || component.isRendering) {
|
|
7
|
-
throw new Error("Only use
|
|
7
|
+
throw new Error("Only use useEffect in component setup");
|
|
8
8
|
}
|
|
9
9
|
let disposer;
|
|
10
10
|
const observer = new Observer(() => {
|
package/dist/useRef.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useRef.d.ts","sourceRoot":"","sources":["../src/useRef.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"useRef.d.ts","sourceRoot":"","sources":["../src/useRef.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAIpC,wBAAgB,MAAM,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,CAoBxC"}
|
package/dist/useRef.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { getCurrentObserver, Signal } from "./observation";
|
|
2
|
+
import { getCurrentComponent } from "./component";
|
|
2
3
|
export function useRef() {
|
|
3
4
|
let value = null;
|
|
5
|
+
const currentComponent = getCurrentComponent();
|
|
6
|
+
if (!currentComponent || currentComponent.isRendering) {
|
|
7
|
+
throw new Error("Only use useRef in component setup");
|
|
8
|
+
}
|
|
4
9
|
const signal = new Signal();
|
|
5
10
|
return {
|
|
6
11
|
get current() {
|
package/dist/useRouter.js
CHANGED
|
@@ -3,7 +3,7 @@ import { getCurrentObserver, Signal } from "./observation";
|
|
|
3
3
|
import { useCleanup, getCurrentComponent } from "./component";
|
|
4
4
|
export function useRouter(config, options) {
|
|
5
5
|
if (!getCurrentComponent()) {
|
|
6
|
-
throw new Error("Only use
|
|
6
|
+
throw new Error("Only use useRouter in component setup");
|
|
7
7
|
}
|
|
8
8
|
const router = internalCreateRouter(config, options);
|
|
9
9
|
const signal = new Signal();
|
package/dist/useState.d.ts
CHANGED
|
@@ -9,14 +9,14 @@ export declare function assignState<T extends object>(state: T, newState: T): T;
|
|
|
9
9
|
* @example
|
|
10
10
|
* // ❌ Bad - destructuring loses reactivity
|
|
11
11
|
* function Component(props) {
|
|
12
|
-
* const state =
|
|
12
|
+
* const state = useState({ count: 0, name: "foo" });
|
|
13
13
|
* const { count, name } = state; // Don't do this!
|
|
14
14
|
* return () => <div>{count} {name}</div>; // Won't update!
|
|
15
15
|
* }
|
|
16
16
|
*
|
|
17
17
|
* // ✅ Good - access properties directly in render
|
|
18
18
|
* function Component(props) {
|
|
19
|
-
* const state =
|
|
19
|
+
* const state = useState({ count: 0, name: "foo" });
|
|
20
20
|
* return () => <div>{state.count} {state.name}</div>; // Reactive!
|
|
21
21
|
* }
|
|
22
22
|
*
|
package/dist/useState.js
CHANGED
|
@@ -14,14 +14,14 @@ export function assignState(state, newState) {
|
|
|
14
14
|
* @example
|
|
15
15
|
* // ❌ Bad - destructuring loses reactivity
|
|
16
16
|
* function Component(props) {
|
|
17
|
-
* const state =
|
|
17
|
+
* const state = useState({ count: 0, name: "foo" });
|
|
18
18
|
* const { count, name } = state; // Don't do this!
|
|
19
19
|
* return () => <div>{count} {name}</div>; // Won't update!
|
|
20
20
|
* }
|
|
21
21
|
*
|
|
22
22
|
* // ✅ Good - access properties directly in render
|
|
23
23
|
* function Component(props) {
|
|
24
|
-
* const state =
|
|
24
|
+
* const state = useState({ count: 0, name: "foo" });
|
|
25
25
|
* return () => <div>{state.count} {state.name}</div>; // Reactive!
|
|
26
26
|
* }
|
|
27
27
|
*
|
|
@@ -30,7 +30,7 @@ export function assignState(state, newState) {
|
|
|
30
30
|
*/
|
|
31
31
|
export function useState(state) {
|
|
32
32
|
if (getCurrentComponent()?.isRendering) {
|
|
33
|
-
throw new Error("
|
|
33
|
+
throw new Error("useState cannot be called during render. Call it in component setup or globally.");
|
|
34
34
|
}
|
|
35
35
|
return getProxy(state, {});
|
|
36
36
|
}
|
package/dist/useView.js
CHANGED
|
@@ -2,7 +2,7 @@ import { getCurrentComponent } from "./component";
|
|
|
2
2
|
import { INSPECT_MARKER, INSPECTOR_ENABLED } from "./inspect";
|
|
3
3
|
export function useView(...args) {
|
|
4
4
|
if (!getCurrentComponent()) {
|
|
5
|
-
throw new Error("Only use
|
|
5
|
+
throw new Error("Only use useView in component setup");
|
|
6
6
|
}
|
|
7
7
|
const result = {};
|
|
8
8
|
const seen = new Set();
|