@assistant-ui/tap 0.6.1 → 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 +14 -14
- 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.js +11 -11
- 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 +10 -9
- 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 +1 -1
- 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,488 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests to verify when React strict mode causes double-rendering
|
|
3
|
-
* for different sources of setState calls
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
7
|
-
import { render, fireEvent, waitFor } from "@testing-library/react";
|
|
8
|
-
import { StrictMode, useState, useEffect, useLayoutEffect } from "react";
|
|
9
|
-
|
|
10
|
-
describe("React Strict Mode - Rerender Sources", () => {
|
|
11
|
-
describe("Source 1: Initial render", () => {
|
|
12
|
-
it("should double-render on initial mount", () => {
|
|
13
|
-
const events: string[] = [];
|
|
14
|
-
|
|
15
|
-
function TestComponent() {
|
|
16
|
-
const [count] = useState(0);
|
|
17
|
-
events.push(`render count=${count}`);
|
|
18
|
-
return <div>{count}</div>;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
render(
|
|
22
|
-
<StrictMode>
|
|
23
|
-
<TestComponent />
|
|
24
|
-
</StrictMode>,
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
expect(events).toEqual(["render count=0", "render count=0"]);
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
describe("Source 2: setState in render", () => {
|
|
32
|
-
it("should handle setState during render", () => {
|
|
33
|
-
const events: string[] = [];
|
|
34
|
-
|
|
35
|
-
function TestComponent() {
|
|
36
|
-
const [count, setCount] = useState(0);
|
|
37
|
-
events.push(`render count=${count}`);
|
|
38
|
-
|
|
39
|
-
// setState during render (this pattern sets state once during initial render)
|
|
40
|
-
if (count === 0) {
|
|
41
|
-
setCount(1);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return <div>{count}</div>;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
render(
|
|
48
|
-
<StrictMode>
|
|
49
|
-
<TestComponent />
|
|
50
|
-
</StrictMode>,
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
// ACTUAL: setState during render only renders once with old value,
|
|
54
|
-
// then double-renders with new value
|
|
55
|
-
expect(events).toEqual([
|
|
56
|
-
"render count=0",
|
|
57
|
-
"render count=1",
|
|
58
|
-
"render count=1",
|
|
59
|
-
]);
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
describe("Source 3: setState in useEffect", () => {
|
|
64
|
-
it("should double-render after setState in useEffect", () => {
|
|
65
|
-
const events: string[] = [];
|
|
66
|
-
|
|
67
|
-
function TestComponent() {
|
|
68
|
-
const [count, setCount] = useState(0);
|
|
69
|
-
events.push(`render count=${count}`);
|
|
70
|
-
|
|
71
|
-
useEffect(() => {
|
|
72
|
-
events.push(`effect count=${count}`);
|
|
73
|
-
if (count === 0) {
|
|
74
|
-
setCount(1);
|
|
75
|
-
}
|
|
76
|
-
return () => {
|
|
77
|
-
events.push(`cleanup count=${count}`);
|
|
78
|
-
};
|
|
79
|
-
}, [count]);
|
|
80
|
-
|
|
81
|
-
return <div>{count}</div>;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
render(
|
|
85
|
-
<StrictMode>
|
|
86
|
-
<TestComponent />
|
|
87
|
-
</StrictMode>,
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
expect(events).toEqual([
|
|
91
|
-
"render count=0",
|
|
92
|
-
"render count=0",
|
|
93
|
-
"effect count=0",
|
|
94
|
-
"cleanup count=0",
|
|
95
|
-
"effect count=0",
|
|
96
|
-
"render count=1",
|
|
97
|
-
"render count=1",
|
|
98
|
-
"cleanup count=0",
|
|
99
|
-
"effect count=1",
|
|
100
|
-
]);
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
describe("Source 4: setState in event handler", () => {
|
|
105
|
-
it("should ALSO double-render after setState in event handler (React 19)", () => {
|
|
106
|
-
const events: string[] = [];
|
|
107
|
-
|
|
108
|
-
function TestComponent() {
|
|
109
|
-
const [count, setCount] = useState(0);
|
|
110
|
-
events.push(`render count=${count}`);
|
|
111
|
-
|
|
112
|
-
return (
|
|
113
|
-
<button
|
|
114
|
-
type="button"
|
|
115
|
-
onClick={() => {
|
|
116
|
-
events.push("click");
|
|
117
|
-
setCount(count + 1);
|
|
118
|
-
}}
|
|
119
|
-
>
|
|
120
|
-
{count}
|
|
121
|
-
</button>
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const { getByRole } = render(
|
|
126
|
-
<StrictMode>
|
|
127
|
-
<TestComponent />
|
|
128
|
-
</StrictMode>,
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
// Initial render is double
|
|
132
|
-
expect(events).toEqual(["render count=0", "render count=0"]);
|
|
133
|
-
|
|
134
|
-
events.length = 0; // Clear events
|
|
135
|
-
|
|
136
|
-
// Click the button
|
|
137
|
-
fireEvent.click(getByRole("button"));
|
|
138
|
-
|
|
139
|
-
// ACTUAL: In React 19 strict mode, ALL renders are doubled!
|
|
140
|
-
// Even renders triggered by event handlers!
|
|
141
|
-
expect(events).toEqual(["click", "render count=1", "render count=1"]);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("should double-render on ALL event handler clicks (React 19)", () => {
|
|
145
|
-
const events: string[] = [];
|
|
146
|
-
|
|
147
|
-
function TestComponent() {
|
|
148
|
-
const [count, setCount] = useState(0);
|
|
149
|
-
events.push(`render count=${count}`);
|
|
150
|
-
|
|
151
|
-
return (
|
|
152
|
-
<button
|
|
153
|
-
type="button"
|
|
154
|
-
onClick={() => {
|
|
155
|
-
events.push("click");
|
|
156
|
-
setCount((c) => c + 1);
|
|
157
|
-
}}
|
|
158
|
-
>
|
|
159
|
-
{count}
|
|
160
|
-
</button>
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const { getByRole } = render(
|
|
165
|
-
<StrictMode>
|
|
166
|
-
<TestComponent />
|
|
167
|
-
</StrictMode>,
|
|
168
|
-
);
|
|
169
|
-
|
|
170
|
-
events.length = 0; // Clear initial renders
|
|
171
|
-
|
|
172
|
-
// Multiple clicks
|
|
173
|
-
fireEvent.click(getByRole("button"));
|
|
174
|
-
fireEvent.click(getByRole("button"));
|
|
175
|
-
fireEvent.click(getByRole("button"));
|
|
176
|
-
|
|
177
|
-
// ACTUAL: Each click causes DOUBLE render in React 19 strict mode
|
|
178
|
-
expect(events).toEqual([
|
|
179
|
-
"click",
|
|
180
|
-
"render count=1",
|
|
181
|
-
"render count=1",
|
|
182
|
-
"click",
|
|
183
|
-
"render count=2",
|
|
184
|
-
"render count=2",
|
|
185
|
-
"click",
|
|
186
|
-
"render count=3",
|
|
187
|
-
"render count=3",
|
|
188
|
-
]);
|
|
189
|
-
});
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
describe("Source 5: setState in setTimeout", () => {
|
|
193
|
-
afterEach(() => {
|
|
194
|
-
vi.useRealTimers();
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it("should double-render AND double-call setTimeout callback (React 19)", async () => {
|
|
198
|
-
// Use fake timers so both strict-mode setTimeout callbacks fire
|
|
199
|
-
// synchronously before React gets a chance to flush a re-render
|
|
200
|
-
// between them. Without this, slow CI can process the first
|
|
201
|
-
// setTimeout, run its renders, and only then fire the second.
|
|
202
|
-
vi.useFakeTimers();
|
|
203
|
-
|
|
204
|
-
const events: string[] = [];
|
|
205
|
-
|
|
206
|
-
function TestComponent() {
|
|
207
|
-
const [count, setCount] = useState(0);
|
|
208
|
-
events.push(`render count=${count}`);
|
|
209
|
-
|
|
210
|
-
useEffect(() => {
|
|
211
|
-
if (count === 0) {
|
|
212
|
-
setTimeout(() => {
|
|
213
|
-
events.push("setTimeout");
|
|
214
|
-
setCount(1);
|
|
215
|
-
}, 10);
|
|
216
|
-
}
|
|
217
|
-
}, [count]);
|
|
218
|
-
|
|
219
|
-
return <div>{count}</div>;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
render(
|
|
223
|
-
<StrictMode>
|
|
224
|
-
<TestComponent />
|
|
225
|
-
</StrictMode>,
|
|
226
|
-
);
|
|
227
|
-
|
|
228
|
-
// Fire both setTimeout callbacks synchronously via fake timers
|
|
229
|
-
vi.advanceTimersByTime(10);
|
|
230
|
-
// Restore real timers and wait for React's scheduler (MessageChannel) to flush
|
|
231
|
-
vi.useRealTimers();
|
|
232
|
-
await waitFor(() => {
|
|
233
|
-
expect(events).toHaveLength(6);
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
// ACTUAL: setTimeout callback runs TWICE and renders are DOUBLED
|
|
237
|
-
expect(events).toEqual([
|
|
238
|
-
"render count=0",
|
|
239
|
-
"render count=0",
|
|
240
|
-
"setTimeout",
|
|
241
|
-
"setTimeout",
|
|
242
|
-
"render count=1",
|
|
243
|
-
"render count=1",
|
|
244
|
-
]);
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
describe("Source 6: setState in Promise/async", () => {
|
|
249
|
-
it("should double-render AND double-call Promise callback (React 19)", async () => {
|
|
250
|
-
const events: string[] = [];
|
|
251
|
-
|
|
252
|
-
function TestComponent() {
|
|
253
|
-
const [count, setCount] = useState(0);
|
|
254
|
-
events.push(`render count=${count}`);
|
|
255
|
-
|
|
256
|
-
useEffect(() => {
|
|
257
|
-
if (count === 0) {
|
|
258
|
-
Promise.resolve().then(() => {
|
|
259
|
-
events.push("promise");
|
|
260
|
-
setCount(1);
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
}, [count]);
|
|
264
|
-
|
|
265
|
-
return <div>{count}</div>;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
render(
|
|
269
|
-
<StrictMode>
|
|
270
|
-
<TestComponent />
|
|
271
|
-
</StrictMode>,
|
|
272
|
-
);
|
|
273
|
-
|
|
274
|
-
// Wait for promise
|
|
275
|
-
await waitFor(() => {
|
|
276
|
-
expect(events).toContain("promise");
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
// ACTUAL: Promise callback runs TWICE and renders are DOUBLED
|
|
280
|
-
expect(events).toEqual([
|
|
281
|
-
"render count=0",
|
|
282
|
-
"render count=0",
|
|
283
|
-
"promise",
|
|
284
|
-
"promise",
|
|
285
|
-
"render count=1",
|
|
286
|
-
"render count=1",
|
|
287
|
-
]);
|
|
288
|
-
});
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
describe("Source 7: Multiple setState calls", () => {
|
|
292
|
-
it("should batch multiple setState calls in event handlers (single render)", () => {
|
|
293
|
-
const events: string[] = [];
|
|
294
|
-
|
|
295
|
-
function TestComponent() {
|
|
296
|
-
const [count1, setCount1] = useState(0);
|
|
297
|
-
const [count2, setCount2] = useState(0);
|
|
298
|
-
events.push(`render count1=${count1} count2=${count2}`);
|
|
299
|
-
|
|
300
|
-
return (
|
|
301
|
-
<button
|
|
302
|
-
type="button"
|
|
303
|
-
onClick={() => {
|
|
304
|
-
events.push("click");
|
|
305
|
-
setCount1(1);
|
|
306
|
-
setCount2(2);
|
|
307
|
-
}}
|
|
308
|
-
>
|
|
309
|
-
Click
|
|
310
|
-
</button>
|
|
311
|
-
);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const { getByRole } = render(
|
|
315
|
-
<StrictMode>
|
|
316
|
-
<TestComponent />
|
|
317
|
-
</StrictMode>,
|
|
318
|
-
);
|
|
319
|
-
|
|
320
|
-
events.length = 0; // Clear initial renders
|
|
321
|
-
|
|
322
|
-
fireEvent.click(getByRole("button"));
|
|
323
|
-
|
|
324
|
-
// ACTUAL: Both setState calls batched, but render is DOUBLED
|
|
325
|
-
expect(events).toEqual([
|
|
326
|
-
"click",
|
|
327
|
-
"render count1=1 count2=2",
|
|
328
|
-
"render count1=1 count2=2",
|
|
329
|
-
]);
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
it("should batch multiple setState calls in useEffect (single double-render)", () => {
|
|
333
|
-
const events: string[] = [];
|
|
334
|
-
|
|
335
|
-
function TestComponent() {
|
|
336
|
-
const [count1, setCount1] = useState(0);
|
|
337
|
-
const [count2, setCount2] = useState(0);
|
|
338
|
-
events.push(`render count1=${count1} count2=${count2}`);
|
|
339
|
-
|
|
340
|
-
useEffect(() => {
|
|
341
|
-
if (count1 === 0 && count2 === 0) {
|
|
342
|
-
setCount1(1);
|
|
343
|
-
setCount2(2);
|
|
344
|
-
}
|
|
345
|
-
}, [count1, count2]);
|
|
346
|
-
|
|
347
|
-
return <div>Test</div>;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
render(
|
|
351
|
-
<StrictMode>
|
|
352
|
-
<TestComponent />
|
|
353
|
-
</StrictMode>,
|
|
354
|
-
);
|
|
355
|
-
|
|
356
|
-
// Initial double-render, then batched setState causes another double-render
|
|
357
|
-
expect(events).toEqual([
|
|
358
|
-
"render count1=0 count2=0",
|
|
359
|
-
"render count1=0 count2=0",
|
|
360
|
-
"render count1=1 count2=2",
|
|
361
|
-
"render count1=1 count2=2",
|
|
362
|
-
]);
|
|
363
|
-
});
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
describe("Source 8: setState in useLayoutEffect", () => {
|
|
367
|
-
it("should double-render after setState in useLayoutEffect", () => {
|
|
368
|
-
const events: string[] = [];
|
|
369
|
-
|
|
370
|
-
function TestComponent() {
|
|
371
|
-
const [count, setCount] = useState(0);
|
|
372
|
-
events.push(`render count=${count}`);
|
|
373
|
-
|
|
374
|
-
useLayoutEffect(() => {
|
|
375
|
-
events.push(`layoutEffect count=${count}`);
|
|
376
|
-
if (count === 0) {
|
|
377
|
-
setCount(1);
|
|
378
|
-
}
|
|
379
|
-
return () => {
|
|
380
|
-
events.push(`layoutCleanup count=${count}`);
|
|
381
|
-
};
|
|
382
|
-
}, [count]);
|
|
383
|
-
|
|
384
|
-
return <div>{count}</div>;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
render(
|
|
388
|
-
<StrictMode>
|
|
389
|
-
<TestComponent />
|
|
390
|
-
</StrictMode>,
|
|
391
|
-
);
|
|
392
|
-
|
|
393
|
-
// useLayoutEffect runs synchronously after render, before paint
|
|
394
|
-
expect(events).toEqual([
|
|
395
|
-
"render count=0",
|
|
396
|
-
"render count=0",
|
|
397
|
-
"layoutEffect count=0",
|
|
398
|
-
"layoutCleanup count=0",
|
|
399
|
-
"layoutEffect count=0",
|
|
400
|
-
"render count=1",
|
|
401
|
-
"render count=1",
|
|
402
|
-
"layoutCleanup count=0",
|
|
403
|
-
"layoutEffect count=1",
|
|
404
|
-
]);
|
|
405
|
-
});
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
describe("Source 9: Effect with dependencies calling setState (derived state)", () => {
|
|
409
|
-
it("should handle effect with dependencies and setState", () => {
|
|
410
|
-
const events: string[] = [];
|
|
411
|
-
|
|
412
|
-
function TestComponent() {
|
|
413
|
-
const [count] = useState(0);
|
|
414
|
-
const [doubled, setDoubled] = useState(0);
|
|
415
|
-
events.push(`render count=${count} doubled=${doubled}`);
|
|
416
|
-
|
|
417
|
-
useEffect(() => {
|
|
418
|
-
events.push(`effect count=${count}`);
|
|
419
|
-
setDoubled(count * 2);
|
|
420
|
-
return () => {
|
|
421
|
-
events.push(`cleanup count=${count}`);
|
|
422
|
-
};
|
|
423
|
-
}, [count]);
|
|
424
|
-
|
|
425
|
-
return <div>{doubled}</div>;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
render(
|
|
429
|
-
<StrictMode>
|
|
430
|
-
<TestComponent />
|
|
431
|
-
</StrictMode>,
|
|
432
|
-
);
|
|
433
|
-
|
|
434
|
-
// setDoubled(0*2) = setDoubled(0) is a no-op, so no extra render
|
|
435
|
-
expect(events).toEqual([
|
|
436
|
-
"render count=0 doubled=0",
|
|
437
|
-
"render count=0 doubled=0",
|
|
438
|
-
"effect count=0",
|
|
439
|
-
"cleanup count=0",
|
|
440
|
-
"effect count=0",
|
|
441
|
-
]);
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
it("should handle effect with dependencies and setState after state change", () => {
|
|
445
|
-
const events: string[] = [];
|
|
446
|
-
|
|
447
|
-
function TestComponent() {
|
|
448
|
-
const [count, setCount] = useState(0);
|
|
449
|
-
const [doubled, setDoubled] = useState(0);
|
|
450
|
-
events.push(`render count=${count} doubled=${doubled}`);
|
|
451
|
-
|
|
452
|
-
useEffect(() => {
|
|
453
|
-
events.push(`effect count=${count}`);
|
|
454
|
-
setDoubled(count * 2);
|
|
455
|
-
return () => {
|
|
456
|
-
events.push(`cleanup count=${count}`);
|
|
457
|
-
};
|
|
458
|
-
}, [count]);
|
|
459
|
-
|
|
460
|
-
return (
|
|
461
|
-
<button type="button" onClick={() => setCount((c) => c + 1)}>
|
|
462
|
-
Click
|
|
463
|
-
</button>
|
|
464
|
-
);
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
const { getByRole } = render(
|
|
468
|
-
<StrictMode>
|
|
469
|
-
<TestComponent />
|
|
470
|
-
</StrictMode>,
|
|
471
|
-
);
|
|
472
|
-
|
|
473
|
-
events.length = 0;
|
|
474
|
-
|
|
475
|
-
fireEvent.click(getByRole("button"));
|
|
476
|
-
|
|
477
|
-
// Double-render with new count, effect sets doubled=2, triggers another double-render
|
|
478
|
-
expect(events).toEqual([
|
|
479
|
-
"render count=1 doubled=0",
|
|
480
|
-
"render count=1 doubled=0",
|
|
481
|
-
"cleanup count=0",
|
|
482
|
-
"effect count=1",
|
|
483
|
-
"render count=1 doubled=2",
|
|
484
|
-
"render count=1 doubled=2",
|
|
485
|
-
]);
|
|
486
|
-
});
|
|
487
|
-
});
|
|
488
|
-
});
|