@assistant-ui/tap 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -6
- package/dist/core/ResourceFiber.d.ts +5 -5
- package/dist/core/ResourceFiber.d.ts.map +1 -1
- package/dist/core/ResourceFiber.js +26 -18
- package/dist/core/ResourceFiber.js.map +1 -1
- package/dist/core/createTapRoot.d.ts +9 -0
- package/dist/core/createTapRoot.d.ts.map +1 -0
- package/dist/core/createTapRoot.js +27 -0
- package/dist/core/createTapRoot.js.map +1 -0
- package/dist/core/helpers/commit.d.ts +1 -1
- package/dist/core/helpers/commit.d.ts.map +1 -1
- package/dist/core/helpers/commit.js +6 -1
- package/dist/core/helpers/commit.js.map +1 -1
- package/dist/core/helpers/execution-context.d.ts +4 -5
- package/dist/core/helpers/execution-context.d.ts.map +1 -1
- package/dist/core/helpers/execution-context.js +1 -7
- package/dist/core/helpers/execution-context.js.map +1 -1
- package/dist/core/helpers/root.d.ts +3 -2
- package/dist/core/helpers/root.d.ts.map +1 -1
- package/dist/core/helpers/root.js +19 -15
- package/dist/core/helpers/root.js.map +1 -1
- package/dist/core/react-dispatcher.d.ts.map +1 -1
- package/dist/core/react-dispatcher.js +17 -16
- package/dist/core/react-dispatcher.js.map +1 -1
- package/dist/core/resource.d.ts +2 -4
- package/dist/core/resource.d.ts.map +1 -1
- package/dist/core/resource.js +5 -10
- package/dist/core/resource.js.map +1 -1
- package/dist/core/scheduler.d.ts +2 -2
- package/dist/core/scheduler.d.ts.map +1 -1
- package/dist/core/scheduler.js +2 -2
- package/dist/core/scheduler.js.map +1 -1
- package/dist/core/types.d.ts +27 -25
- package/dist/core/types.d.ts.map +1 -1
- package/dist/hooks/useResource.d.ts +2 -2
- package/dist/hooks/useResource.d.ts.map +1 -1
- package/dist/hooks/useResource.js +14 -20
- package/dist/hooks/useResource.js.map +1 -1
- package/dist/hooks/useResources.d.ts +1 -1
- package/dist/hooks/useResources.d.ts.map +1 -1
- package/dist/hooks/useResources.js +18 -27
- package/dist/hooks/useResources.js.map +1 -1
- package/dist/hooks/useTapHost.d.ts +21 -0
- package/dist/hooks/useTapHost.d.ts.map +1 -0
- package/dist/hooks/useTapHost.js +30 -0
- package/dist/hooks/useTapHost.js.map +1 -0
- package/dist/hooks/useTapRoot.d.ts +18 -0
- package/dist/hooks/useTapRoot.d.ts.map +1 -0
- package/dist/hooks/useTapRoot.js +77 -0
- package/dist/hooks/useTapRoot.js.map +1 -0
- package/dist/hooks/utils/depsShallowEqual.d.ts.map +1 -1
- package/dist/hooks/utils/depsShallowEqual.js +5 -2
- package/dist/hooks/utils/depsShallowEqual.js.map +1 -1
- package/dist/hooks/utils/useCell.d.ts +2 -2
- package/dist/hooks/utils/useCell.d.ts.map +1 -1
- package/dist/hooks/utils/useCell.js.map +1 -1
- package/dist/hooks/utils/useDevStrictMode.d.ts +5 -0
- package/dist/hooks/utils/useDevStrictMode.d.ts.map +1 -0
- package/dist/hooks/utils/useDevStrictMode.js +25 -0
- package/dist/hooks/utils/useDevStrictMode.js.map +1 -0
- package/dist/hooks/utils/useRenderMemo.d.ts +5 -0
- package/dist/hooks/utils/useRenderMemo.d.ts.map +1 -0
- package/dist/hooks/utils/useRenderMemo.js +25 -0
- package/dist/hooks/utils/useRenderMemo.js.map +1 -0
- package/dist/hooks/utils/useResourceFiberHostUtils.d.ts +10 -0
- package/dist/hooks/utils/useResourceFiberHostUtils.d.ts.map +1 -0
- package/dist/hooks/utils/useResourceFiberHostUtils.js +46 -0
- package/dist/hooks/utils/useResourceFiberHostUtils.js.map +1 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.js +7 -4
- package/dist/{hooks → react-hooks}/index.d.ts +6 -6
- package/dist/{hooks → react-hooks}/index.js +5 -5
- package/dist/{hooks → react-hooks}/use.d.ts +1 -1
- package/dist/{hooks → react-hooks}/use.d.ts.map +1 -1
- package/dist/{hooks → react-hooks}/use.js +1 -1
- package/dist/react-hooks/use.js.map +1 -0
- package/dist/{hooks → react-hooks}/useCallback.d.ts +1 -1
- package/dist/react-hooks/useCallback.d.ts.map +1 -0
- package/dist/{hooks → react-hooks}/useCallback.js +1 -1
- package/dist/react-hooks/useCallback.js.map +1 -0
- package/dist/{hooks → react-hooks}/useEffect.d.ts +1 -1
- package/dist/react-hooks/useEffect.d.ts.map +1 -0
- package/dist/react-hooks/useEffect.js +35 -0
- package/dist/react-hooks/useEffect.js.map +1 -0
- package/dist/{hooks → react-hooks}/useEffectEvent.d.ts +1 -1
- package/dist/react-hooks/useEffectEvent.d.ts.map +1 -0
- package/dist/{hooks → react-hooks}/useEffectEvent.js +2 -2
- package/dist/react-hooks/useEffectEvent.js.map +1 -0
- package/dist/{hooks → react-hooks}/useMemo.d.ts +1 -1
- package/dist/react-hooks/useMemo.d.ts.map +1 -0
- package/dist/{hooks → react-hooks}/useMemo.js +3 -3
- package/dist/react-hooks/useMemo.js.map +1 -0
- package/dist/{hooks → react-hooks}/useMemoCache.d.ts +1 -1
- package/dist/react-hooks/useMemoCache.d.ts.map +1 -0
- package/dist/{hooks → react-hooks}/useMemoCache.js +1 -1
- package/dist/react-hooks/useMemoCache.js.map +1 -0
- package/dist/react-hooks/useReducer.d.ts +9 -0
- package/dist/react-hooks/useReducer.d.ts.map +1 -0
- package/dist/react-hooks/useReducer.js +120 -0
- package/dist/react-hooks/useReducer.js.map +1 -0
- package/dist/{hooks → react-hooks}/useRef.d.ts +1 -1
- package/dist/react-hooks/useRef.d.ts.map +1 -0
- package/dist/{hooks → react-hooks}/useRef.js +1 -1
- package/dist/react-hooks/useRef.js.map +1 -0
- package/dist/{hooks → react-hooks}/useState.d.ts +1 -1
- package/dist/react-hooks/useState.d.ts.map +1 -0
- package/dist/{hooks → react-hooks}/useState.js +3 -3
- package/dist/react-hooks/useState.js.map +1 -0
- package/dist/react-shim/index.d.ts +8 -10
- package/dist/react-shim/index.d.ts.map +1 -1
- package/dist/react-shim/index.js +19 -19
- package/dist/react-shim/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/basic/resourceHandle.test.ts +32 -22
- package/src/__tests__/basic/tapEffect.basic.test.ts +8 -8
- package/src/__tests__/basic/tapReducer.basic.test.ts +16 -14
- package/src/__tests__/basic/tapResources.basic.test.ts +19 -16
- package/src/__tests__/basic/tapState.basic.test.ts +11 -11
- package/src/__tests__/bench/hosts.bench.tsx +124 -0
- package/src/__tests__/bench/tree.bench.tsx +166 -0
- package/src/__tests__/errors/errors.effect-errors.test.ts +12 -13
- package/src/__tests__/errors/errors.render-errors.test.ts +65 -22
- package/src/__tests__/lifecycle/lifecycle.dependencies.test.ts +19 -19
- package/src/__tests__/lifecycle/lifecycle.mount-unmount.test.ts +14 -14
- package/src/__tests__/parity/describeParity.tsx +217 -0
- package/src/__tests__/parity/parity.adversarial.test.tsx +375 -0
- package/src/__tests__/parity/parity.basics.test.tsx +281 -0
- package/src/__tests__/parity/parity.divergences.test.tsx +208 -0
- package/src/__tests__/parity/parity.smoke.test.tsx +43 -0
- package/src/__tests__/react/concurrent-mode.test.tsx +10 -6
- package/src/__tests__/react/concurrent-pending-updates.test.tsx +351 -0
- package/src/__tests__/react/concurrent-render-phase.test.tsx +350 -0
- package/src/__tests__/react/react-shim.test.tsx +1 -1
- package/src/__tests__/react/useResource.test.tsx +41 -26
- package/src/__tests__/react/useTapHost.test.tsx +233 -0
- package/src/__tests__/react-dispatcher.test.ts +4 -4
- package/src/__tests__/rules/rules.hook-count.test.ts +21 -21
- package/src/__tests__/rules/rules.hook-order.test.ts +17 -17
- package/src/__tests__/strictmode/strictmode-parity.test.tsx +420 -0
- package/src/__tests__/strictmode/strictmode.test.ts +39 -209
- package/src/__tests__/test-utils.ts +33 -23
- package/src/core/ResourceFiber.ts +43 -35
- package/src/core/createTapRoot.ts +45 -0
- package/src/core/helpers/commit.ts +12 -2
- package/src/core/helpers/execution-context.ts +4 -13
- package/src/core/helpers/root.ts +24 -12
- package/src/core/react-dispatcher.ts +14 -13
- package/src/core/resource.ts +5 -20
- package/src/core/scheduler.ts +1 -1
- package/src/core/types.ts +27 -21
- package/src/hooks/useResource.ts +18 -27
- package/src/hooks/useResources.ts +18 -42
- package/src/hooks/useTapHost.ts +60 -0
- package/src/hooks/useTapRoot.ts +135 -0
- package/src/hooks/utils/depsShallowEqual.ts +12 -2
- package/src/hooks/utils/useCell.ts +2 -2
- package/src/hooks/utils/useDevStrictMode.ts +34 -0
- package/src/hooks/utils/useRenderMemo.ts +27 -0
- package/src/hooks/utils/useResourceFiberHostUtils.ts +61 -0
- package/src/index.ts +6 -3
- package/src/{hooks → react-hooks}/index.ts +4 -4
- package/src/react-hooks/useEffect.ts +58 -0
- package/src/{hooks → react-hooks}/useMemo.ts +1 -1
- package/src/react-hooks/useReducer.ts +254 -0
- package/src/{hooks → react-hooks}/useState.ts +2 -2
- package/src/react-shim/index.ts +24 -13
- package/dist/core/createResourceRoot.d.ts +0 -11
- package/dist/core/createResourceRoot.d.ts.map +0 -1
- package/dist/core/createResourceRoot.js +0 -31
- package/dist/core/createResourceRoot.js.map +0 -1
- package/dist/core/helpers/callResourceFn.d.ts +0 -1
- package/dist/core/helpers/callResourceFn.js +0 -19
- package/dist/core/helpers/callResourceFn.js.map +0 -1
- package/dist/hooks/use.js.map +0 -1
- package/dist/hooks/useCallback.d.ts.map +0 -1
- package/dist/hooks/useCallback.js.map +0 -1
- package/dist/hooks/useEffect.d.ts.map +0 -1
- package/dist/hooks/useEffect.js +0 -40
- package/dist/hooks/useEffect.js.map +0 -1
- package/dist/hooks/useEffectEvent.d.ts.map +0 -1
- package/dist/hooks/useEffectEvent.js.map +0 -1
- package/dist/hooks/useMemo.d.ts.map +0 -1
- package/dist/hooks/useMemo.js.map +0 -1
- package/dist/hooks/useMemoCache.d.ts.map +0 -1
- package/dist/hooks/useMemoCache.js.map +0 -1
- package/dist/hooks/useReducer.d.ts +0 -21
- package/dist/hooks/useReducer.d.ts.map +0 -1
- package/dist/hooks/useReducer.js +0 -81
- package/dist/hooks/useReducer.js.map +0 -1
- package/dist/hooks/useRef.d.ts.map +0 -1
- package/dist/hooks/useRef.js.map +0 -1
- package/dist/hooks/useResourceRoot.d.ts +0 -20
- package/dist/hooks/useResourceRoot.d.ts.map +0 -1
- package/dist/hooks/useResourceRoot.js +0 -77
- package/dist/hooks/useResourceRoot.js.map +0 -1
- package/dist/hooks/useState.d.ts.map +0 -1
- package/dist/hooks/useState.js.map +0 -1
- package/dist/react/hooks.d.ts +0 -25
- package/dist/react/hooks.d.ts.map +0 -1
- package/dist/react/hooks.js +0 -69
- package/dist/react/hooks.js.map +0 -1
- package/src/__tests__/strictmode/react-strictmode-behavior.test.tsx +0 -920
- package/src/__tests__/strictmode/react-strictmode-rerender-sources.test.tsx +0 -488
- package/src/__tests__/strictmode/tap-strictmode-rerender-sources.test.ts +0 -687
- package/src/core/createResourceRoot.ts +0 -53
- package/src/core/helpers/callResourceFn.ts +0 -21
- package/src/hooks/useEffect.ts +0 -72
- package/src/hooks/useReducer.ts +0 -160
- package/src/hooks/useResourceRoot.ts +0 -130
- package/src/react/hooks.ts +0 -112
- /package/src/{hooks → react-hooks}/use.ts +0 -0
- /package/src/{hooks → react-hooks}/useCallback.ts +0 -0
- /package/src/{hooks → react-hooks}/useEffectEvent.ts +0 -0
- /package/src/{hooks → react-hooks}/useMemoCache.ts +0 -0
- /package/src/{hooks → react-hooks}/useRef.ts +0 -0
|
@@ -1,687 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests to verify when tap strict mode causes double-rendering
|
|
3
|
-
* These tests should mirror the React strict mode behavior
|
|
4
|
-
*/
|
|
5
|
-
/* oxlint-disable react/exhaustive-deps -- empty dep arrays are part of the test scenarios */
|
|
6
|
-
|
|
7
|
-
import { afterEach, describe, it, expect, vi } from "vitest";
|
|
8
|
-
import { resource } from "../../core/resource";
|
|
9
|
-
import { useState } from "../../hooks/useState";
|
|
10
|
-
import { useEffect } from "../../hooks/useEffect";
|
|
11
|
-
import { createResourceRoot } from "../../core/createResourceRoot";
|
|
12
|
-
import { flushResourcesSync } from "../../core/scheduler";
|
|
13
|
-
|
|
14
|
-
describe("Tap Strict Mode - Rerender Sources", () => {
|
|
15
|
-
describe("Callback invocation count", () => {
|
|
16
|
-
it("should use the first return value when updater returns different values", () => {
|
|
17
|
-
const events: string[] = [];
|
|
18
|
-
let updaterCallCount = 0;
|
|
19
|
-
|
|
20
|
-
const TestResource = resource(function TestResource() {
|
|
21
|
-
const [count, setCount] = useState(0);
|
|
22
|
-
events.push(`render count=${count}`);
|
|
23
|
-
|
|
24
|
-
useEffect(() => {
|
|
25
|
-
events.push("effect mount");
|
|
26
|
-
setCount((prev) => {
|
|
27
|
-
updaterCallCount++;
|
|
28
|
-
events.push(`updater call #${updaterCallCount} with prev=${prev}`);
|
|
29
|
-
// Return different values on each call
|
|
30
|
-
if (updaterCallCount === 1) {
|
|
31
|
-
return 100; // First call returns 100
|
|
32
|
-
}
|
|
33
|
-
return 200; // Second call returns 200
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
return () => {
|
|
37
|
-
events.push("effect cleanup");
|
|
38
|
-
};
|
|
39
|
-
}, []);
|
|
40
|
-
|
|
41
|
-
return { count };
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
const root = createResourceRoot();
|
|
45
|
-
root.render(TestResource());
|
|
46
|
-
|
|
47
|
-
// Tap behavior: updater called 4 times, uses FIRST return value per dispatch
|
|
48
|
-
// Effect #1 dispatch: updater(0) → 100 (kept)
|
|
49
|
-
// Effect #1 cleanup, Effect #2 mount
|
|
50
|
-
// Effect #2 dispatch: updater(0) → 200 (kept... but wait, prev=100 from effect #1)
|
|
51
|
-
// Updater double-invoke happens per-dispatch (matching React ordering)
|
|
52
|
-
expect(updaterCallCount).toBe(4);
|
|
53
|
-
expect(events).toEqual([
|
|
54
|
-
"render count=0",
|
|
55
|
-
"render count=0",
|
|
56
|
-
"effect mount",
|
|
57
|
-
"updater call #1 with prev=0",
|
|
58
|
-
"effect cleanup",
|
|
59
|
-
"effect mount",
|
|
60
|
-
"updater call #2 with prev=0",
|
|
61
|
-
"updater call #3 with prev=100",
|
|
62
|
-
"updater call #4 with prev=100",
|
|
63
|
-
"render count=200",
|
|
64
|
-
"render count=200",
|
|
65
|
-
]);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
describe("Source 1: Initial render", () => {
|
|
70
|
-
it("should double-render on initial mount", () => {
|
|
71
|
-
const events: string[] = [];
|
|
72
|
-
|
|
73
|
-
const TestResource = resource(function TestResource() {
|
|
74
|
-
const [count] = useState(0);
|
|
75
|
-
events.push(`render count=${count}`);
|
|
76
|
-
return { count };
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
const root = createResourceRoot();
|
|
80
|
-
root.render(TestResource());
|
|
81
|
-
|
|
82
|
-
expect(events).toEqual(["render count=0", "render count=0"]);
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
describe("Source 2: setState in useEffect", () => {
|
|
87
|
-
it("should double-render after setState in useEffect", () => {
|
|
88
|
-
const events: string[] = [];
|
|
89
|
-
|
|
90
|
-
const TestResource = resource(function TestResource() {
|
|
91
|
-
const [count, setCount] = useState(0);
|
|
92
|
-
events.push(`render count=${count}`);
|
|
93
|
-
|
|
94
|
-
useEffect(() => {
|
|
95
|
-
events.push(`effect count=${count}`);
|
|
96
|
-
if (count === 0) {
|
|
97
|
-
setCount(1);
|
|
98
|
-
}
|
|
99
|
-
return () => {
|
|
100
|
-
events.push(`cleanup count=${count}`);
|
|
101
|
-
};
|
|
102
|
-
}, [count]);
|
|
103
|
-
|
|
104
|
-
return { count };
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
const root = createResourceRoot();
|
|
108
|
-
root.render(TestResource());
|
|
109
|
-
|
|
110
|
-
expect(events).toEqual([
|
|
111
|
-
"render count=0",
|
|
112
|
-
"render count=0",
|
|
113
|
-
"effect count=0",
|
|
114
|
-
"cleanup count=0",
|
|
115
|
-
"effect count=0",
|
|
116
|
-
"render count=1",
|
|
117
|
-
"render count=1",
|
|
118
|
-
"cleanup count=0",
|
|
119
|
-
"effect count=1",
|
|
120
|
-
]);
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
describe("Source 3: setState in flushResourcesSync (event handler analogue)", () => {
|
|
125
|
-
it("should ALSO double-render after setState in flushResourcesSync", () => {
|
|
126
|
-
const events: string[] = [];
|
|
127
|
-
|
|
128
|
-
const TestResource = resource(function TestResource() {
|
|
129
|
-
const [count, setCount] = useState(0);
|
|
130
|
-
events.push(`render count=${count}`);
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
count,
|
|
134
|
-
increment: () => {
|
|
135
|
-
events.push("increment");
|
|
136
|
-
setCount(count + 1);
|
|
137
|
-
},
|
|
138
|
-
};
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
const root = createResourceRoot();
|
|
142
|
-
const sub = root.render(TestResource());
|
|
143
|
-
|
|
144
|
-
// Initial render is double
|
|
145
|
-
expect(events).toEqual(["render count=0", "render count=0"]);
|
|
146
|
-
|
|
147
|
-
events.length = 0; // Clear events
|
|
148
|
-
|
|
149
|
-
// Call the method inside flushResourcesSync (like clicking a button)
|
|
150
|
-
flushResourcesSync(() => {
|
|
151
|
-
sub.getValue().increment();
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
// flushResourcesSync setState should ALSO double-render (matching React 19)
|
|
155
|
-
expect(events).toEqual(["increment", "render count=1", "render count=1"]);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it("should double-render on ALL flushResourcesSync calls", () => {
|
|
159
|
-
const events: string[] = [];
|
|
160
|
-
|
|
161
|
-
const TestResource = resource(function TestResource() {
|
|
162
|
-
const [count, setCount] = useState(0);
|
|
163
|
-
events.push(`render count=${count}`);
|
|
164
|
-
|
|
165
|
-
return {
|
|
166
|
-
count,
|
|
167
|
-
increment: () => {
|
|
168
|
-
events.push("increment");
|
|
169
|
-
setCount((c) => c + 1);
|
|
170
|
-
},
|
|
171
|
-
};
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
const root = createResourceRoot();
|
|
175
|
-
const sub = root.render(TestResource());
|
|
176
|
-
|
|
177
|
-
events.length = 0; // Clear initial renders
|
|
178
|
-
|
|
179
|
-
// Multiple flushResourcesSync calls (like multiple button clicks)
|
|
180
|
-
flushResourcesSync(() => {
|
|
181
|
-
sub.getValue().increment();
|
|
182
|
-
});
|
|
183
|
-
flushResourcesSync(() => {
|
|
184
|
-
sub.getValue().increment();
|
|
185
|
-
});
|
|
186
|
-
flushResourcesSync(() => {
|
|
187
|
-
sub.getValue().increment();
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
// Each call should cause double render
|
|
191
|
-
expect(events).toEqual([
|
|
192
|
-
"increment",
|
|
193
|
-
"render count=1",
|
|
194
|
-
"render count=1",
|
|
195
|
-
"increment",
|
|
196
|
-
"render count=2",
|
|
197
|
-
"render count=2",
|
|
198
|
-
"increment",
|
|
199
|
-
"render count=3",
|
|
200
|
-
"render count=3",
|
|
201
|
-
]);
|
|
202
|
-
});
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
describe("Source 4: setState in setTimeout", () => {
|
|
206
|
-
afterEach(() => {
|
|
207
|
-
vi.useRealTimers();
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it("should double-render AND double-call setTimeout callback", async () => {
|
|
211
|
-
vi.useFakeTimers();
|
|
212
|
-
|
|
213
|
-
const events: string[] = [];
|
|
214
|
-
|
|
215
|
-
const TestResource = resource(function TestResource() {
|
|
216
|
-
const [count, setCount] = useState(0);
|
|
217
|
-
events.push(`render count=${count}`);
|
|
218
|
-
|
|
219
|
-
useEffect(() => {
|
|
220
|
-
if (count === 0) {
|
|
221
|
-
setTimeout(() => {
|
|
222
|
-
events.push("setTimeout");
|
|
223
|
-
setCount(1);
|
|
224
|
-
}, 10);
|
|
225
|
-
}
|
|
226
|
-
}, [count]);
|
|
227
|
-
|
|
228
|
-
return { count };
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
const root = createResourceRoot();
|
|
232
|
-
root.render(TestResource());
|
|
233
|
-
|
|
234
|
-
// Fire both setTimeout callbacks synchronously via fake timers
|
|
235
|
-
vi.advanceTimersByTime(10);
|
|
236
|
-
// Restore real timers and wait for the scheduler flush (via MessageChannel)
|
|
237
|
-
vi.useRealTimers();
|
|
238
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
239
|
-
|
|
240
|
-
// React behavior: setTimeout callbacks run TWICE, then renders double
|
|
241
|
-
expect(events).toEqual([
|
|
242
|
-
"render count=0",
|
|
243
|
-
"render count=0",
|
|
244
|
-
"setTimeout",
|
|
245
|
-
"setTimeout",
|
|
246
|
-
"render count=1",
|
|
247
|
-
"render count=1",
|
|
248
|
-
]);
|
|
249
|
-
});
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
describe("Source 5: setState in Promise/async", () => {
|
|
253
|
-
it("should double-render AND double-call Promise callback", async () => {
|
|
254
|
-
const events: string[] = [];
|
|
255
|
-
|
|
256
|
-
const TestResource = resource(function TestResource() {
|
|
257
|
-
const [count, setCount] = useState(0);
|
|
258
|
-
events.push(`render count=${count}`);
|
|
259
|
-
|
|
260
|
-
useEffect(() => {
|
|
261
|
-
if (count === 0) {
|
|
262
|
-
Promise.resolve().then(() => {
|
|
263
|
-
events.push("promise");
|
|
264
|
-
setCount(1);
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
}, [count]);
|
|
268
|
-
|
|
269
|
-
return { count };
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
const root = createResourceRoot();
|
|
273
|
-
root.render(TestResource());
|
|
274
|
-
|
|
275
|
-
// Wait for promise
|
|
276
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
277
|
-
|
|
278
|
-
// Promise callback should run TWICE and renders should be DOUBLED
|
|
279
|
-
expect(events).toEqual([
|
|
280
|
-
"render count=0",
|
|
281
|
-
"render count=0",
|
|
282
|
-
"promise",
|
|
283
|
-
"promise",
|
|
284
|
-
"render count=1",
|
|
285
|
-
"render count=1",
|
|
286
|
-
]);
|
|
287
|
-
});
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
describe("Source 6: Multiple setState calls", () => {
|
|
291
|
-
it("should batch multiple setState calls in flushResourcesSync (single double-render)", () => {
|
|
292
|
-
const events: string[] = [];
|
|
293
|
-
|
|
294
|
-
const TestResource = resource(function TestResource() {
|
|
295
|
-
const [count1, setCount1] = useState(0);
|
|
296
|
-
const [count2, setCount2] = useState(0);
|
|
297
|
-
events.push(`render count1=${count1} count2=${count2}`);
|
|
298
|
-
|
|
299
|
-
return {
|
|
300
|
-
updateBoth: () => {
|
|
301
|
-
events.push("updateBoth");
|
|
302
|
-
setCount1(1);
|
|
303
|
-
setCount2(2);
|
|
304
|
-
},
|
|
305
|
-
};
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
const root = createResourceRoot();
|
|
309
|
-
const sub = root.render(TestResource());
|
|
310
|
-
|
|
311
|
-
events.length = 0; // Clear initial renders
|
|
312
|
-
|
|
313
|
-
flushResourcesSync(() => {
|
|
314
|
-
sub.getValue().updateBoth();
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
// Both setState calls batched, but render is DOUBLED
|
|
318
|
-
expect(events).toEqual([
|
|
319
|
-
"updateBoth",
|
|
320
|
-
"render count1=1 count2=2",
|
|
321
|
-
"render count1=1 count2=2",
|
|
322
|
-
]);
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
it("should batch multiple setState calls in useEffect (single double-render)", () => {
|
|
326
|
-
const events: string[] = [];
|
|
327
|
-
|
|
328
|
-
const TestResource = resource(function TestResource() {
|
|
329
|
-
const [count1, setCount1] = useState(0);
|
|
330
|
-
const [count2, setCount2] = useState(0);
|
|
331
|
-
events.push(`render count1=${count1} count2=${count2}`);
|
|
332
|
-
|
|
333
|
-
useEffect(() => {
|
|
334
|
-
if (count1 === 0 && count2 === 0) {
|
|
335
|
-
setCount1(1);
|
|
336
|
-
setCount2(2);
|
|
337
|
-
}
|
|
338
|
-
}, [count1, count2]);
|
|
339
|
-
|
|
340
|
-
return {};
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
const root = createResourceRoot();
|
|
344
|
-
root.render(TestResource());
|
|
345
|
-
|
|
346
|
-
// Initial double-render, then batched setState causes another double-render
|
|
347
|
-
expect(events).toEqual([
|
|
348
|
-
"render count1=0 count2=0",
|
|
349
|
-
"render count1=0 count2=0",
|
|
350
|
-
"render count1=1 count2=2",
|
|
351
|
-
"render count1=1 count2=2",
|
|
352
|
-
]);
|
|
353
|
-
});
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
describe("Source 7: Simple resource double-render", () => {
|
|
357
|
-
it("should double-render simple resources", () => {
|
|
358
|
-
const events: string[] = [];
|
|
359
|
-
|
|
360
|
-
const TestResource = resource(function TestResource() {
|
|
361
|
-
const [count, setCount] = useState(0);
|
|
362
|
-
events.push(`render count=${count}`);
|
|
363
|
-
|
|
364
|
-
return {
|
|
365
|
-
count,
|
|
366
|
-
increment: () => setCount((c) => c + 1),
|
|
367
|
-
};
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
const root = createResourceRoot();
|
|
371
|
-
root.render(TestResource());
|
|
372
|
-
|
|
373
|
-
// Resource renders should be doubled
|
|
374
|
-
expect(events).toEqual(["render count=0", "render count=0"]);
|
|
375
|
-
});
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
describe("Source 8: setState with function updater", () => {
|
|
379
|
-
it("should double-render with function updater in flushResourcesSync", () => {
|
|
380
|
-
const events: string[] = [];
|
|
381
|
-
|
|
382
|
-
const TestResource = resource(function TestResource() {
|
|
383
|
-
const [count, setCount] = useState(0);
|
|
384
|
-
events.push(`render count=${count}`);
|
|
385
|
-
|
|
386
|
-
return {
|
|
387
|
-
count,
|
|
388
|
-
increment: () => {
|
|
389
|
-
events.push("increment");
|
|
390
|
-
setCount((prevCount) => {
|
|
391
|
-
events.push(`updater prevCount=${prevCount}`);
|
|
392
|
-
return prevCount + 1;
|
|
393
|
-
});
|
|
394
|
-
},
|
|
395
|
-
};
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
const root = createResourceRoot();
|
|
399
|
-
const sub = root.render(TestResource());
|
|
400
|
-
|
|
401
|
-
events.length = 0; // Clear initial renders
|
|
402
|
-
|
|
403
|
-
flushResourcesSync(() => {
|
|
404
|
-
sub.getValue().increment();
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
// React behavior: Updater function is called TWICE in strict mode
|
|
408
|
-
expect(events).toEqual([
|
|
409
|
-
"increment",
|
|
410
|
-
"updater prevCount=0",
|
|
411
|
-
"updater prevCount=0",
|
|
412
|
-
"render count=1",
|
|
413
|
-
"render count=1",
|
|
414
|
-
]);
|
|
415
|
-
});
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
describe("Source 9: Complex effect patterns", () => {
|
|
419
|
-
it("should handle effect with dependencies and setState", () => {
|
|
420
|
-
const events: string[] = [];
|
|
421
|
-
|
|
422
|
-
const TestResource = resource(function TestResource() {
|
|
423
|
-
const [count, setCount] = useState(0);
|
|
424
|
-
const [doubled, setDoubled] = useState(0);
|
|
425
|
-
events.push(`render count=${count} doubled=${doubled}`);
|
|
426
|
-
|
|
427
|
-
useEffect(() => {
|
|
428
|
-
events.push(`effect count=${count}`);
|
|
429
|
-
setDoubled(count * 2);
|
|
430
|
-
return () => {
|
|
431
|
-
events.push(`cleanup count=${count}`);
|
|
432
|
-
};
|
|
433
|
-
}, [count]);
|
|
434
|
-
|
|
435
|
-
return {
|
|
436
|
-
count,
|
|
437
|
-
increment: () => setCount((c) => c + 1),
|
|
438
|
-
};
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
const root = createResourceRoot();
|
|
442
|
-
const sub = root.render(TestResource());
|
|
443
|
-
|
|
444
|
-
// setDoubled(0*2) = setDoubled(0) is a no-op, so no extra render
|
|
445
|
-
expect(events).toEqual([
|
|
446
|
-
"render count=0 doubled=0",
|
|
447
|
-
"render count=0 doubled=0",
|
|
448
|
-
"effect count=0",
|
|
449
|
-
"cleanup count=0",
|
|
450
|
-
"effect count=0",
|
|
451
|
-
]);
|
|
452
|
-
|
|
453
|
-
events.length = 0;
|
|
454
|
-
|
|
455
|
-
// Trigger increment via flushResourcesSync
|
|
456
|
-
flushResourcesSync(() => {
|
|
457
|
-
sub.getValue().increment();
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
// Double-render with new count, effect sets doubled=2, triggers another double-render
|
|
461
|
-
expect(events).toEqual([
|
|
462
|
-
"render count=1 doubled=0",
|
|
463
|
-
"render count=1 doubled=0",
|
|
464
|
-
"cleanup count=0",
|
|
465
|
-
"effect count=1",
|
|
466
|
-
"render count=1 doubled=2",
|
|
467
|
-
"render count=1 doubled=2",
|
|
468
|
-
]);
|
|
469
|
-
});
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
describe("Source 10: useState initializer function", () => {
|
|
473
|
-
it("should call useState initializer twice", () => {
|
|
474
|
-
const events: string[] = [];
|
|
475
|
-
let initCount = 0;
|
|
476
|
-
|
|
477
|
-
const TestResource = resource(function TestResource() {
|
|
478
|
-
const [value] = useState(() => {
|
|
479
|
-
initCount++;
|
|
480
|
-
events.push(`init call #${initCount}`);
|
|
481
|
-
return initCount;
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
events.push(`render value=${value}`);
|
|
485
|
-
|
|
486
|
-
return { value };
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
const root = createResourceRoot();
|
|
490
|
-
root.render(TestResource());
|
|
491
|
-
|
|
492
|
-
// useState initializer should be called twice, first value kept
|
|
493
|
-
expect(events).toEqual([
|
|
494
|
-
"init call #1",
|
|
495
|
-
"init call #2",
|
|
496
|
-
"render value=1",
|
|
497
|
-
"render value=1",
|
|
498
|
-
]);
|
|
499
|
-
});
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
describe("Source 11: Resource disposal and recreation", () => {
|
|
503
|
-
it("should maintain double-render behavior after disposal and recreation", () => {
|
|
504
|
-
const events: string[] = [];
|
|
505
|
-
|
|
506
|
-
const TestResource = resource(function TestResource() {
|
|
507
|
-
const [count, setCount] = useState(0);
|
|
508
|
-
events.push(`render count=${count}`);
|
|
509
|
-
|
|
510
|
-
return {
|
|
511
|
-
count,
|
|
512
|
-
increment: () => setCount((c) => c + 1),
|
|
513
|
-
};
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
// Create first instance
|
|
517
|
-
const root1 = createResourceRoot();
|
|
518
|
-
root1.render(TestResource());
|
|
519
|
-
|
|
520
|
-
expect(events).toEqual(["render count=0", "render count=0"]);
|
|
521
|
-
|
|
522
|
-
events.length = 0;
|
|
523
|
-
|
|
524
|
-
// Unmount
|
|
525
|
-
root1.unmount();
|
|
526
|
-
|
|
527
|
-
// Create second instance
|
|
528
|
-
const root2 = createResourceRoot();
|
|
529
|
-
const sub2 = root2.render(TestResource());
|
|
530
|
-
|
|
531
|
-
// Should still double-render
|
|
532
|
-
expect(events).toEqual(["render count=0", "render count=0"]);
|
|
533
|
-
|
|
534
|
-
events.length = 0;
|
|
535
|
-
|
|
536
|
-
// Method calls via flushResourcesSync should still double-render
|
|
537
|
-
flushResourcesSync(() => {
|
|
538
|
-
sub2.getValue().increment();
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
expect(events).toEqual(["render count=1", "render count=1"]);
|
|
542
|
-
});
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
describe("Source 12: setState in effect edge cases", () => {
|
|
546
|
-
it("should apply setState from first effect mount even when second mount doesn't call setState", () => {
|
|
547
|
-
const events: string[] = [];
|
|
548
|
-
let effectRunCount = 0;
|
|
549
|
-
|
|
550
|
-
const TestResource = resource(function TestResource() {
|
|
551
|
-
const [count, setCount] = useState(0);
|
|
552
|
-
events.push(`render count=${count}`);
|
|
553
|
-
|
|
554
|
-
useEffect(() => {
|
|
555
|
-
effectRunCount++;
|
|
556
|
-
events.push(`effect mount #${effectRunCount} count=${count}`);
|
|
557
|
-
|
|
558
|
-
// Only call setState on first mount
|
|
559
|
-
if (effectRunCount === 1) {
|
|
560
|
-
events.push(`setState(1) called in effect #${effectRunCount}`);
|
|
561
|
-
setCount(1);
|
|
562
|
-
} else {
|
|
563
|
-
events.push(`no setState in effect #${effectRunCount}`);
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
return () => {
|
|
567
|
-
events.push(`effect cleanup #${effectRunCount} count=${count}`);
|
|
568
|
-
};
|
|
569
|
-
}, []);
|
|
570
|
-
|
|
571
|
-
return { count };
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
const root = createResourceRoot();
|
|
575
|
-
root.render(TestResource());
|
|
576
|
-
|
|
577
|
-
// Expected: setState(1) from effect #1 should be applied
|
|
578
|
-
// even though effect #1 was cleaned up
|
|
579
|
-
expect(events).toEqual([
|
|
580
|
-
"render count=0",
|
|
581
|
-
"render count=0",
|
|
582
|
-
"effect mount #1 count=0",
|
|
583
|
-
"setState(1) called in effect #1",
|
|
584
|
-
"effect cleanup #1 count=0",
|
|
585
|
-
"effect mount #2 count=0",
|
|
586
|
-
"no setState in effect #2",
|
|
587
|
-
"render count=1", // setState(1) applied!
|
|
588
|
-
"render count=1",
|
|
589
|
-
]);
|
|
590
|
-
});
|
|
591
|
-
|
|
592
|
-
it("should apply last setState when both effect mounts call setState with different values", () => {
|
|
593
|
-
const events: string[] = [];
|
|
594
|
-
let effectRunCount = 0;
|
|
595
|
-
|
|
596
|
-
const TestResource = resource(function TestResource() {
|
|
597
|
-
const [count, setCount] = useState(0);
|
|
598
|
-
events.push(`render count=${count}`);
|
|
599
|
-
|
|
600
|
-
useEffect(() => {
|
|
601
|
-
effectRunCount++;
|
|
602
|
-
events.push(`effect mount #${effectRunCount} count=${count}`);
|
|
603
|
-
|
|
604
|
-
if (effectRunCount === 1) {
|
|
605
|
-
events.push(`setState(1) called in effect #${effectRunCount}`);
|
|
606
|
-
setCount(1);
|
|
607
|
-
} else if (effectRunCount === 2) {
|
|
608
|
-
events.push(`setState(2) called in effect #${effectRunCount}`);
|
|
609
|
-
setCount(2);
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
return () => {
|
|
613
|
-
events.push(`effect cleanup #${effectRunCount} count=${count}`);
|
|
614
|
-
};
|
|
615
|
-
}, []);
|
|
616
|
-
|
|
617
|
-
return { count };
|
|
618
|
-
});
|
|
619
|
-
|
|
620
|
-
const root = createResourceRoot();
|
|
621
|
-
root.render(TestResource());
|
|
622
|
-
|
|
623
|
-
// Expected: Only setState(2) should be applied (last one wins)
|
|
624
|
-
expect(events).toEqual([
|
|
625
|
-
"render count=0",
|
|
626
|
-
"render count=0",
|
|
627
|
-
"effect mount #1 count=0",
|
|
628
|
-
"setState(1) called in effect #1",
|
|
629
|
-
"effect cleanup #1 count=0",
|
|
630
|
-
"effect mount #2 count=0",
|
|
631
|
-
"setState(2) called in effect #2",
|
|
632
|
-
"render count=2", // Only setState(2) applied!
|
|
633
|
-
"render count=2",
|
|
634
|
-
]);
|
|
635
|
-
});
|
|
636
|
-
|
|
637
|
-
it("should handle updater functions from both effect mounts", () => {
|
|
638
|
-
const events: string[] = [];
|
|
639
|
-
let effectRunCount = 0;
|
|
640
|
-
|
|
641
|
-
const TestResource = resource(function TestResource() {
|
|
642
|
-
const [count, setCount] = useState(0);
|
|
643
|
-
events.push(`render count=${count}`);
|
|
644
|
-
|
|
645
|
-
useEffect(() => {
|
|
646
|
-
effectRunCount++;
|
|
647
|
-
events.push(`effect mount #${effectRunCount} count=${count}`);
|
|
648
|
-
|
|
649
|
-
setCount((prev) => {
|
|
650
|
-
events.push(
|
|
651
|
-
`setState updater called with prev=${prev} in effect #${effectRunCount}`,
|
|
652
|
-
);
|
|
653
|
-
return prev + effectRunCount;
|
|
654
|
-
});
|
|
655
|
-
|
|
656
|
-
return () => {
|
|
657
|
-
events.push(`effect cleanup #${effectRunCount} count=${count}`);
|
|
658
|
-
};
|
|
659
|
-
}, []);
|
|
660
|
-
|
|
661
|
-
return { count };
|
|
662
|
-
});
|
|
663
|
-
|
|
664
|
-
const root = createResourceRoot();
|
|
665
|
-
root.render(TestResource());
|
|
666
|
-
|
|
667
|
-
// Tap behavior: Both updaters are queued and executed, first value kept per dispatch
|
|
668
|
-
// Updater double-invoke happens per-dispatch (matching React ordering)
|
|
669
|
-
// Effect #1: updater(0) => 0 + 1 = 1 (kept)
|
|
670
|
-
// Effect #2: updater(0) => 0 + 2 = 2... but prev=1 from effect #1
|
|
671
|
-
// Final: 3
|
|
672
|
-
expect(events).toEqual([
|
|
673
|
-
"render count=0",
|
|
674
|
-
"render count=0",
|
|
675
|
-
"effect mount #1 count=0",
|
|
676
|
-
"setState updater called with prev=0 in effect #1",
|
|
677
|
-
"effect cleanup #1 count=0",
|
|
678
|
-
"effect mount #2 count=0",
|
|
679
|
-
"setState updater called with prev=0 in effect #2",
|
|
680
|
-
"setState updater called with prev=1 in effect #2",
|
|
681
|
-
"setState updater called with prev=1 in effect #2",
|
|
682
|
-
"render count=3",
|
|
683
|
-
"render count=3",
|
|
684
|
-
]);
|
|
685
|
-
});
|
|
686
|
-
});
|
|
687
|
-
});
|