@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.
Files changed (213) hide show
  1. package/README.md +9 -6
  2. package/dist/core/ResourceFiber.d.ts +5 -5
  3. package/dist/core/ResourceFiber.d.ts.map +1 -1
  4. package/dist/core/ResourceFiber.js +26 -18
  5. package/dist/core/ResourceFiber.js.map +1 -1
  6. package/dist/core/createTapRoot.d.ts +9 -0
  7. package/dist/core/createTapRoot.d.ts.map +1 -0
  8. package/dist/core/createTapRoot.js +27 -0
  9. package/dist/core/createTapRoot.js.map +1 -0
  10. package/dist/core/helpers/commit.d.ts +1 -1
  11. package/dist/core/helpers/commit.d.ts.map +1 -1
  12. package/dist/core/helpers/commit.js +6 -1
  13. package/dist/core/helpers/commit.js.map +1 -1
  14. package/dist/core/helpers/execution-context.d.ts +4 -5
  15. package/dist/core/helpers/execution-context.d.ts.map +1 -1
  16. package/dist/core/helpers/execution-context.js +1 -7
  17. package/dist/core/helpers/execution-context.js.map +1 -1
  18. package/dist/core/helpers/root.d.ts +3 -2
  19. package/dist/core/helpers/root.d.ts.map +1 -1
  20. package/dist/core/helpers/root.js +19 -15
  21. package/dist/core/helpers/root.js.map +1 -1
  22. package/dist/core/react-dispatcher.d.ts.map +1 -1
  23. package/dist/core/react-dispatcher.js +14 -14
  24. package/dist/core/react-dispatcher.js.map +1 -1
  25. package/dist/core/resource.d.ts +2 -4
  26. package/dist/core/resource.d.ts.map +1 -1
  27. package/dist/core/resource.js +5 -10
  28. package/dist/core/resource.js.map +1 -1
  29. package/dist/core/scheduler.d.ts +2 -2
  30. package/dist/core/scheduler.d.ts.map +1 -1
  31. package/dist/core/scheduler.js +2 -2
  32. package/dist/core/scheduler.js.map +1 -1
  33. package/dist/core/types.d.ts +27 -25
  34. package/dist/core/types.d.ts.map +1 -1
  35. package/dist/hooks/useResource.d.ts +2 -2
  36. package/dist/hooks/useResource.d.ts.map +1 -1
  37. package/dist/hooks/useResource.js +14 -20
  38. package/dist/hooks/useResource.js.map +1 -1
  39. package/dist/hooks/useResources.d.ts +1 -1
  40. package/dist/hooks/useResources.d.ts.map +1 -1
  41. package/dist/hooks/useResources.js +18 -27
  42. package/dist/hooks/useResources.js.map +1 -1
  43. package/dist/hooks/useTapHost.d.ts +21 -0
  44. package/dist/hooks/useTapHost.d.ts.map +1 -0
  45. package/dist/hooks/useTapHost.js +30 -0
  46. package/dist/hooks/useTapHost.js.map +1 -0
  47. package/dist/hooks/useTapRoot.d.ts +18 -0
  48. package/dist/hooks/useTapRoot.d.ts.map +1 -0
  49. package/dist/hooks/useTapRoot.js +77 -0
  50. package/dist/hooks/useTapRoot.js.map +1 -0
  51. package/dist/hooks/utils/depsShallowEqual.d.ts.map +1 -1
  52. package/dist/hooks/utils/depsShallowEqual.js +5 -2
  53. package/dist/hooks/utils/depsShallowEqual.js.map +1 -1
  54. package/dist/hooks/utils/useCell.d.ts +2 -2
  55. package/dist/hooks/utils/useCell.d.ts.map +1 -1
  56. package/dist/hooks/utils/useCell.js.map +1 -1
  57. package/dist/hooks/utils/useDevStrictMode.d.ts +5 -0
  58. package/dist/hooks/utils/useDevStrictMode.d.ts.map +1 -0
  59. package/dist/hooks/utils/useDevStrictMode.js +25 -0
  60. package/dist/hooks/utils/useDevStrictMode.js.map +1 -0
  61. package/dist/hooks/utils/useRenderMemo.d.ts +5 -0
  62. package/dist/hooks/utils/useRenderMemo.d.ts.map +1 -0
  63. package/dist/hooks/utils/useRenderMemo.js +25 -0
  64. package/dist/hooks/utils/useRenderMemo.js.map +1 -0
  65. package/dist/hooks/utils/useResourceFiberHostUtils.d.ts +10 -0
  66. package/dist/hooks/utils/useResourceFiberHostUtils.d.ts.map +1 -0
  67. package/dist/hooks/utils/useResourceFiberHostUtils.js +46 -0
  68. package/dist/hooks/utils/useResourceFiberHostUtils.js.map +1 -0
  69. package/dist/index.d.ts +7 -4
  70. package/dist/index.js +7 -4
  71. package/dist/{hooks → react-hooks}/index.d.ts +6 -6
  72. package/dist/{hooks → react-hooks}/index.js +5 -5
  73. package/dist/{hooks → react-hooks}/use.d.ts +1 -1
  74. package/dist/{hooks → react-hooks}/use.d.ts.map +1 -1
  75. package/dist/{hooks → react-hooks}/use.js +1 -1
  76. package/dist/react-hooks/use.js.map +1 -0
  77. package/dist/{hooks → react-hooks}/useCallback.d.ts +1 -1
  78. package/dist/react-hooks/useCallback.d.ts.map +1 -0
  79. package/dist/{hooks → react-hooks}/useCallback.js +1 -1
  80. package/dist/react-hooks/useCallback.js.map +1 -0
  81. package/dist/{hooks → react-hooks}/useEffect.d.ts +1 -1
  82. package/dist/react-hooks/useEffect.d.ts.map +1 -0
  83. package/dist/react-hooks/useEffect.js +35 -0
  84. package/dist/react-hooks/useEffect.js.map +1 -0
  85. package/dist/{hooks → react-hooks}/useEffectEvent.d.ts +1 -1
  86. package/dist/react-hooks/useEffectEvent.d.ts.map +1 -0
  87. package/dist/{hooks → react-hooks}/useEffectEvent.js +2 -2
  88. package/dist/react-hooks/useEffectEvent.js.map +1 -0
  89. package/dist/{hooks → react-hooks}/useMemo.d.ts +1 -1
  90. package/dist/react-hooks/useMemo.d.ts.map +1 -0
  91. package/dist/{hooks → react-hooks}/useMemo.js +3 -3
  92. package/dist/react-hooks/useMemo.js.map +1 -0
  93. package/dist/{hooks → react-hooks}/useMemoCache.d.ts +1 -1
  94. package/dist/react-hooks/useMemoCache.d.ts.map +1 -0
  95. package/dist/{hooks → react-hooks}/useMemoCache.js +1 -1
  96. package/dist/react-hooks/useMemoCache.js.map +1 -0
  97. package/dist/react-hooks/useReducer.d.ts +9 -0
  98. package/dist/react-hooks/useReducer.d.ts.map +1 -0
  99. package/dist/react-hooks/useReducer.js +120 -0
  100. package/dist/react-hooks/useReducer.js.map +1 -0
  101. package/dist/{hooks → react-hooks}/useRef.d.ts +1 -1
  102. package/dist/react-hooks/useRef.d.ts.map +1 -0
  103. package/dist/{hooks → react-hooks}/useRef.js +1 -1
  104. package/dist/react-hooks/useRef.js.map +1 -0
  105. package/dist/{hooks → react-hooks}/useState.d.ts +1 -1
  106. package/dist/react-hooks/useState.d.ts.map +1 -0
  107. package/dist/{hooks → react-hooks}/useState.js +3 -3
  108. package/dist/react-hooks/useState.js.map +1 -0
  109. package/dist/react-shim/index.js +11 -11
  110. package/dist/react-shim/index.js.map +1 -1
  111. package/package.json +1 -1
  112. package/src/__tests__/basic/resourceHandle.test.ts +32 -22
  113. package/src/__tests__/basic/tapEffect.basic.test.ts +8 -8
  114. package/src/__tests__/basic/tapReducer.basic.test.ts +16 -14
  115. package/src/__tests__/basic/tapResources.basic.test.ts +19 -16
  116. package/src/__tests__/basic/tapState.basic.test.ts +11 -11
  117. package/src/__tests__/bench/hosts.bench.tsx +124 -0
  118. package/src/__tests__/bench/tree.bench.tsx +166 -0
  119. package/src/__tests__/errors/errors.effect-errors.test.ts +12 -13
  120. package/src/__tests__/errors/errors.render-errors.test.ts +65 -22
  121. package/src/__tests__/lifecycle/lifecycle.dependencies.test.ts +19 -19
  122. package/src/__tests__/lifecycle/lifecycle.mount-unmount.test.ts +14 -14
  123. package/src/__tests__/parity/describeParity.tsx +217 -0
  124. package/src/__tests__/parity/parity.adversarial.test.tsx +375 -0
  125. package/src/__tests__/parity/parity.basics.test.tsx +281 -0
  126. package/src/__tests__/parity/parity.divergences.test.tsx +208 -0
  127. package/src/__tests__/parity/parity.smoke.test.tsx +43 -0
  128. package/src/__tests__/react/concurrent-mode.test.tsx +10 -6
  129. package/src/__tests__/react/concurrent-pending-updates.test.tsx +351 -0
  130. package/src/__tests__/react/concurrent-render-phase.test.tsx +350 -0
  131. package/src/__tests__/react/react-shim.test.tsx +1 -1
  132. package/src/__tests__/react/useResource.test.tsx +41 -26
  133. package/src/__tests__/react/useTapHost.test.tsx +233 -0
  134. package/src/__tests__/react-dispatcher.test.ts +4 -4
  135. package/src/__tests__/rules/rules.hook-count.test.ts +21 -21
  136. package/src/__tests__/rules/rules.hook-order.test.ts +17 -17
  137. package/src/__tests__/strictmode/strictmode-parity.test.tsx +420 -0
  138. package/src/__tests__/strictmode/strictmode.test.ts +39 -209
  139. package/src/__tests__/test-utils.ts +33 -23
  140. package/src/core/ResourceFiber.ts +43 -35
  141. package/src/core/createTapRoot.ts +45 -0
  142. package/src/core/helpers/commit.ts +12 -2
  143. package/src/core/helpers/execution-context.ts +4 -13
  144. package/src/core/helpers/root.ts +24 -12
  145. package/src/core/react-dispatcher.ts +10 -9
  146. package/src/core/resource.ts +5 -20
  147. package/src/core/scheduler.ts +1 -1
  148. package/src/core/types.ts +27 -21
  149. package/src/hooks/useResource.ts +18 -27
  150. package/src/hooks/useResources.ts +18 -42
  151. package/src/hooks/useTapHost.ts +60 -0
  152. package/src/hooks/useTapRoot.ts +135 -0
  153. package/src/hooks/utils/depsShallowEqual.ts +12 -2
  154. package/src/hooks/utils/useCell.ts +2 -2
  155. package/src/hooks/utils/useDevStrictMode.ts +34 -0
  156. package/src/hooks/utils/useRenderMemo.ts +27 -0
  157. package/src/hooks/utils/useResourceFiberHostUtils.ts +61 -0
  158. package/src/index.ts +6 -3
  159. package/src/{hooks → react-hooks}/index.ts +4 -4
  160. package/src/react-hooks/useEffect.ts +58 -0
  161. package/src/{hooks → react-hooks}/useMemo.ts +1 -1
  162. package/src/react-hooks/useReducer.ts +254 -0
  163. package/src/{hooks → react-hooks}/useState.ts +2 -2
  164. package/src/react-shim/index.ts +1 -1
  165. package/dist/core/createResourceRoot.d.ts +0 -11
  166. package/dist/core/createResourceRoot.d.ts.map +0 -1
  167. package/dist/core/createResourceRoot.js +0 -31
  168. package/dist/core/createResourceRoot.js.map +0 -1
  169. package/dist/core/helpers/callResourceFn.d.ts +0 -1
  170. package/dist/core/helpers/callResourceFn.js +0 -19
  171. package/dist/core/helpers/callResourceFn.js.map +0 -1
  172. package/dist/hooks/use.js.map +0 -1
  173. package/dist/hooks/useCallback.d.ts.map +0 -1
  174. package/dist/hooks/useCallback.js.map +0 -1
  175. package/dist/hooks/useEffect.d.ts.map +0 -1
  176. package/dist/hooks/useEffect.js +0 -40
  177. package/dist/hooks/useEffect.js.map +0 -1
  178. package/dist/hooks/useEffectEvent.d.ts.map +0 -1
  179. package/dist/hooks/useEffectEvent.js.map +0 -1
  180. package/dist/hooks/useMemo.d.ts.map +0 -1
  181. package/dist/hooks/useMemo.js.map +0 -1
  182. package/dist/hooks/useMemoCache.d.ts.map +0 -1
  183. package/dist/hooks/useMemoCache.js.map +0 -1
  184. package/dist/hooks/useReducer.d.ts +0 -21
  185. package/dist/hooks/useReducer.d.ts.map +0 -1
  186. package/dist/hooks/useReducer.js +0 -81
  187. package/dist/hooks/useReducer.js.map +0 -1
  188. package/dist/hooks/useRef.d.ts.map +0 -1
  189. package/dist/hooks/useRef.js.map +0 -1
  190. package/dist/hooks/useResourceRoot.d.ts +0 -20
  191. package/dist/hooks/useResourceRoot.d.ts.map +0 -1
  192. package/dist/hooks/useResourceRoot.js +0 -77
  193. package/dist/hooks/useResourceRoot.js.map +0 -1
  194. package/dist/hooks/useState.d.ts.map +0 -1
  195. package/dist/hooks/useState.js.map +0 -1
  196. package/dist/react/hooks.d.ts +0 -25
  197. package/dist/react/hooks.d.ts.map +0 -1
  198. package/dist/react/hooks.js +0 -69
  199. package/dist/react/hooks.js.map +0 -1
  200. package/src/__tests__/strictmode/react-strictmode-behavior.test.tsx +0 -920
  201. package/src/__tests__/strictmode/react-strictmode-rerender-sources.test.tsx +0 -488
  202. package/src/__tests__/strictmode/tap-strictmode-rerender-sources.test.ts +0 -687
  203. package/src/core/createResourceRoot.ts +0 -53
  204. package/src/core/helpers/callResourceFn.ts +0 -21
  205. package/src/hooks/useEffect.ts +0 -72
  206. package/src/hooks/useReducer.ts +0 -160
  207. package/src/hooks/useResourceRoot.ts +0 -130
  208. package/src/react/hooks.ts +0 -112
  209. /package/src/{hooks → react-hooks}/use.ts +0 -0
  210. /package/src/{hooks → react-hooks}/useCallback.ts +0 -0
  211. /package/src/{hooks → react-hooks}/useEffectEvent.ts +0 -0
  212. /package/src/{hooks → react-hooks}/useMemoCache.ts +0 -0
  213. /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
- });