@assistant-ui/tap 0.3.6 → 0.4.2

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 (124) hide show
  1. package/README.md +24 -23
  2. package/dist/core/ResourceFiber.d.ts +1 -1
  3. package/dist/core/ResourceFiber.d.ts.map +1 -1
  4. package/dist/core/ResourceFiber.js +15 -8
  5. package/dist/core/ResourceFiber.js.map +1 -1
  6. package/dist/core/commit.d.ts +1 -1
  7. package/dist/core/commit.d.ts.map +1 -1
  8. package/dist/core/commit.js +40 -50
  9. package/dist/core/commit.js.map +1 -1
  10. package/dist/core/context.d.ts +2 -2
  11. package/dist/core/context.d.ts.map +1 -1
  12. package/dist/core/context.js +2 -2
  13. package/dist/core/context.js.map +1 -1
  14. package/dist/core/createResource.d.ts +3 -2
  15. package/dist/core/createResource.d.ts.map +1 -1
  16. package/dist/core/createResource.js +48 -22
  17. package/dist/core/createResource.js.map +1 -1
  18. package/dist/core/env.d.ts +2 -0
  19. package/dist/core/env.d.ts.map +1 -0
  20. package/dist/core/env.js +3 -0
  21. package/dist/core/env.js.map +1 -0
  22. package/dist/core/execution-context.d.ts +1 -0
  23. package/dist/core/execution-context.d.ts.map +1 -1
  24. package/dist/core/execution-context.js +8 -0
  25. package/dist/core/execution-context.js.map +1 -1
  26. package/dist/core/resource.d.ts +4 -3
  27. package/dist/core/resource.d.ts.map +1 -1
  28. package/dist/core/resource.js.map +1 -1
  29. package/dist/core/scheduler.d.ts +1 -1
  30. package/dist/core/scheduler.d.ts.map +1 -1
  31. package/dist/core/scheduler.js +4 -1
  32. package/dist/core/scheduler.js.map +1 -1
  33. package/dist/core/types.d.ts +22 -21
  34. package/dist/core/types.d.ts.map +1 -1
  35. package/dist/core/types.js +1 -1
  36. package/dist/core/types.js.map +1 -1
  37. package/dist/core/withKey.d.ts +3 -0
  38. package/dist/core/withKey.d.ts.map +1 -0
  39. package/dist/core/withKey.js +4 -0
  40. package/dist/core/withKey.js.map +1 -0
  41. package/dist/hooks/tap-callback.d.ts.map +1 -1
  42. package/dist/hooks/tap-callback.js +1 -0
  43. package/dist/hooks/tap-callback.js.map +1 -1
  44. package/dist/hooks/tap-const.d.ts +2 -0
  45. package/dist/hooks/tap-const.d.ts.map +1 -0
  46. package/dist/hooks/tap-const.js +6 -0
  47. package/dist/hooks/tap-const.js.map +1 -0
  48. package/dist/hooks/tap-effect-event.d.ts.map +1 -1
  49. package/dist/hooks/tap-effect-event.js +11 -0
  50. package/dist/hooks/tap-effect-event.js.map +1 -1
  51. package/dist/hooks/tap-effect.d.ts.map +1 -1
  52. package/dist/hooks/tap-effect.js +46 -31
  53. package/dist/hooks/tap-effect.js.map +1 -1
  54. package/dist/hooks/tap-inline-resource.d.ts +2 -2
  55. package/dist/hooks/tap-inline-resource.d.ts.map +1 -1
  56. package/dist/hooks/tap-memo.d.ts.map +1 -1
  57. package/dist/hooks/tap-memo.js +9 -1
  58. package/dist/hooks/tap-memo.js.map +1 -1
  59. package/dist/hooks/tap-resource.d.ts +3 -3
  60. package/dist/hooks/tap-resource.d.ts.map +1 -1
  61. package/dist/hooks/tap-resource.js +17 -9
  62. package/dist/hooks/tap-resource.js.map +1 -1
  63. package/dist/hooks/tap-resources.d.ts +2 -10
  64. package/dist/hooks/tap-resources.d.ts.map +1 -1
  65. package/dist/hooks/tap-resources.js +74 -43
  66. package/dist/hooks/tap-resources.js.map +1 -1
  67. package/dist/hooks/tap-state.d.ts.map +1 -1
  68. package/dist/hooks/tap-state.js +37 -24
  69. package/dist/hooks/tap-state.js.map +1 -1
  70. package/dist/hooks/utils/depsShallowEqual.d.ts.map +1 -0
  71. package/dist/hooks/utils/depsShallowEqual.js.map +1 -0
  72. package/dist/hooks/utils/tapHook.d.ts +6 -0
  73. package/dist/hooks/utils/tapHook.d.ts.map +1 -0
  74. package/dist/hooks/utils/tapHook.js +24 -0
  75. package/dist/hooks/utils/tapHook.js.map +1 -0
  76. package/dist/index.d.ts +5 -3
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +4 -2
  79. package/dist/index.js.map +1 -1
  80. package/dist/react/use-resource.d.ts +2 -2
  81. package/dist/react/use-resource.d.ts.map +1 -1
  82. package/dist/react/use-resource.js +24 -10
  83. package/dist/react/use-resource.js.map +1 -1
  84. package/package.json +10 -3
  85. package/src/__tests__/basic/resourceHandle.test.ts +4 -4
  86. package/src/__tests__/basic/tapEffect.basic.test.ts +3 -2
  87. package/src/__tests__/basic/tapResources.basic.test.ts +84 -64
  88. package/src/__tests__/basic/tapState.basic.test.ts +8 -8
  89. package/src/__tests__/errors/errors.effect-errors.test.ts +8 -3
  90. package/src/__tests__/lifecycle/lifecycle.dependencies.test.ts +3 -2
  91. package/src/__tests__/lifecycle/lifecycle.mount-unmount.test.ts +2 -2
  92. package/src/__tests__/react/concurrent-mode.test.tsx +243 -0
  93. package/src/__tests__/strictmode/react-strictmode-behavior.test.tsx +709 -0
  94. package/src/__tests__/strictmode/react-strictmode-rerender-sources.test.tsx +392 -0
  95. package/src/__tests__/strictmode/strictmode.test.ts +274 -0
  96. package/src/__tests__/strictmode/tap-strictmode-rerender-sources.test.ts +723 -0
  97. package/src/__tests__/test-utils.ts +8 -6
  98. package/src/core/ResourceFiber.ts +21 -11
  99. package/src/core/commit.ts +37 -57
  100. package/src/core/context.ts +2 -2
  101. package/src/core/createResource.ts +64 -25
  102. package/src/core/env.ts +3 -0
  103. package/src/core/execution-context.ts +9 -0
  104. package/src/core/resource.ts +9 -3
  105. package/src/core/scheduler.ts +4 -1
  106. package/src/core/types.ts +25 -26
  107. package/src/core/withKey.ts +8 -0
  108. package/src/hooks/tap-callback.ts +1 -0
  109. package/src/hooks/tap-const.ts +6 -0
  110. package/src/hooks/tap-effect-event.ts +15 -0
  111. package/src/hooks/tap-effect.ts +51 -38
  112. package/src/hooks/tap-inline-resource.ts +2 -2
  113. package/src/hooks/tap-memo.ts +10 -1
  114. package/src/hooks/tap-resource.ts +24 -20
  115. package/src/hooks/tap-resources.ts +86 -63
  116. package/src/hooks/tap-state.ts +49 -26
  117. package/src/hooks/utils/tapHook.ts +35 -0
  118. package/src/index.ts +8 -3
  119. package/src/react/use-resource.ts +27 -16
  120. package/dist/hooks/depsShallowEqual.d.ts.map +0 -1
  121. package/dist/hooks/depsShallowEqual.js.map +0 -1
  122. /package/dist/hooks/{depsShallowEqual.d.ts → utils/depsShallowEqual.d.ts} +0 -0
  123. /package/dist/hooks/{depsShallowEqual.js → utils/depsShallowEqual.js} +0 -0
  124. /package/src/hooks/{depsShallowEqual.ts → utils/depsShallowEqual.ts} +0 -0
@@ -0,0 +1,392 @@
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 } from "vitest";
7
+ import { render, fireEvent, waitFor } from "@testing-library/react";
8
+ import { StrictMode, useState, useEffect } 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
+ onClick={() => {
115
+ events.push("click");
116
+ setCount(count + 1);
117
+ }}
118
+ >
119
+ {count}
120
+ </button>
121
+ );
122
+ }
123
+
124
+ const { getByRole } = render(
125
+ <StrictMode>
126
+ <TestComponent />
127
+ </StrictMode>,
128
+ );
129
+
130
+ // Initial render is double
131
+ expect(events).toEqual(["render count=0", "render count=0"]);
132
+
133
+ events.length = 0; // Clear events
134
+
135
+ // Click the button
136
+ fireEvent.click(getByRole("button"));
137
+
138
+ // ACTUAL: In React 19 strict mode, ALL renders are doubled!
139
+ // Even renders triggered by event handlers!
140
+ expect(events).toEqual(["click", "render count=1", "render count=1"]);
141
+ });
142
+
143
+ it("should double-render on ALL event handler clicks (React 19)", () => {
144
+ const events: string[] = [];
145
+
146
+ function TestComponent() {
147
+ const [count, setCount] = useState(0);
148
+ events.push(`render count=${count}`);
149
+
150
+ return (
151
+ <button
152
+ onClick={() => {
153
+ events.push("click");
154
+ setCount((c) => c + 1);
155
+ }}
156
+ >
157
+ {count}
158
+ </button>
159
+ );
160
+ }
161
+
162
+ const { getByRole } = render(
163
+ <StrictMode>
164
+ <TestComponent />
165
+ </StrictMode>,
166
+ );
167
+
168
+ events.length = 0; // Clear initial renders
169
+
170
+ // Multiple clicks
171
+ fireEvent.click(getByRole("button"));
172
+ fireEvent.click(getByRole("button"));
173
+ fireEvent.click(getByRole("button"));
174
+
175
+ // ACTUAL: Each click causes DOUBLE render in React 19 strict mode
176
+ expect(events).toEqual([
177
+ "click",
178
+ "render count=1",
179
+ "render count=1",
180
+ "click",
181
+ "render count=2",
182
+ "render count=2",
183
+ "click",
184
+ "render count=3",
185
+ "render count=3",
186
+ ]);
187
+ });
188
+ });
189
+
190
+ describe("Source 5: setState in setTimeout", () => {
191
+ it("should double-render AND double-call setTimeout callback (React 19)", async () => {
192
+ const events: string[] = [];
193
+
194
+ function TestComponent() {
195
+ const [count, setCount] = useState(0);
196
+ events.push(`render count=${count}`);
197
+
198
+ useEffect(() => {
199
+ if (count === 0) {
200
+ setTimeout(() => {
201
+ events.push("setTimeout");
202
+ setCount(1);
203
+ }, 10);
204
+ }
205
+ }, [count]);
206
+
207
+ return <div>{count}</div>;
208
+ }
209
+
210
+ render(
211
+ <StrictMode>
212
+ <TestComponent />
213
+ </StrictMode>,
214
+ );
215
+
216
+ // Wait for setTimeout
217
+ await waitFor(() => {
218
+ expect(events).toContain("setTimeout");
219
+ });
220
+
221
+ // ACTUAL: setTimeout callback runs TWICE and renders are DOUBLED
222
+ expect(events).toEqual([
223
+ "render count=0",
224
+ "render count=0",
225
+ "setTimeout",
226
+ "setTimeout",
227
+ "render count=1",
228
+ "render count=1",
229
+ ]);
230
+ });
231
+ });
232
+
233
+ describe("Source 6: setState in Promise/async", () => {
234
+ it("should double-render AND double-call Promise callback (React 19)", async () => {
235
+ const events: string[] = [];
236
+
237
+ function TestComponent() {
238
+ const [count, setCount] = useState(0);
239
+ events.push(`render count=${count}`);
240
+
241
+ useEffect(() => {
242
+ if (count === 0) {
243
+ Promise.resolve().then(() => {
244
+ events.push("promise");
245
+ setCount(1);
246
+ });
247
+ }
248
+ }, [count]);
249
+
250
+ return <div>{count}</div>;
251
+ }
252
+
253
+ render(
254
+ <StrictMode>
255
+ <TestComponent />
256
+ </StrictMode>,
257
+ );
258
+
259
+ // Wait for promise
260
+ await waitFor(() => {
261
+ expect(events).toContain("promise");
262
+ });
263
+
264
+ // ACTUAL: Promise callback runs TWICE and renders are DOUBLED
265
+ expect(events).toEqual([
266
+ "render count=0",
267
+ "render count=0",
268
+ "promise",
269
+ "promise",
270
+ "render count=1",
271
+ "render count=1",
272
+ ]);
273
+ });
274
+ });
275
+
276
+ describe("Source 7: Multiple setState calls", () => {
277
+ it("should batch multiple setState calls in event handlers (single render)", () => {
278
+ const events: string[] = [];
279
+
280
+ function TestComponent() {
281
+ const [count1, setCount1] = useState(0);
282
+ const [count2, setCount2] = useState(0);
283
+ events.push(`render count1=${count1} count2=${count2}`);
284
+
285
+ return (
286
+ <button
287
+ onClick={() => {
288
+ events.push("click");
289
+ setCount1(1);
290
+ setCount2(2);
291
+ }}
292
+ >
293
+ Click
294
+ </button>
295
+ );
296
+ }
297
+
298
+ const { getByRole } = render(
299
+ <StrictMode>
300
+ <TestComponent />
301
+ </StrictMode>,
302
+ );
303
+
304
+ events.length = 0; // Clear initial renders
305
+
306
+ fireEvent.click(getByRole("button"));
307
+
308
+ // ACTUAL: Both setState calls batched, but render is DOUBLED
309
+ expect(events).toEqual([
310
+ "click",
311
+ "render count1=1 count2=2",
312
+ "render count1=1 count2=2",
313
+ ]);
314
+ });
315
+
316
+ it("should batch multiple setState calls in useEffect (single double-render)", () => {
317
+ const events: string[] = [];
318
+
319
+ function TestComponent() {
320
+ const [count1, setCount1] = useState(0);
321
+ const [count2, setCount2] = useState(0);
322
+ events.push(`render count1=${count1} count2=${count2}`);
323
+
324
+ useEffect(() => {
325
+ if (count1 === 0 && count2 === 0) {
326
+ setCount1(1);
327
+ setCount2(2);
328
+ }
329
+ }, [count1, count2]);
330
+
331
+ return <div>Test</div>;
332
+ }
333
+
334
+ render(
335
+ <StrictMode>
336
+ <TestComponent />
337
+ </StrictMode>,
338
+ );
339
+
340
+ // Initial double-render, then batched setState causes another double-render
341
+ expect(events).toEqual([
342
+ "render count1=0 count2=0",
343
+ "render count1=0 count2=0",
344
+ "render count1=1 count2=2",
345
+ "render count1=1 count2=2",
346
+ ]);
347
+ });
348
+ });
349
+
350
+ describe("Source 8: setState in useLayoutEffect", () => {
351
+ it("should double-render after setState in useLayoutEffect", () => {
352
+ const events: string[] = [];
353
+ const { useLayoutEffect } = require("react");
354
+
355
+ function TestComponent() {
356
+ const [count, setCount] = useState(0);
357
+ events.push(`render count=${count}`);
358
+
359
+ useLayoutEffect(() => {
360
+ events.push(`layoutEffect count=${count}`);
361
+ if (count === 0) {
362
+ setCount(1);
363
+ }
364
+ return () => {
365
+ events.push(`layoutCleanup count=${count}`);
366
+ };
367
+ }, [count]);
368
+
369
+ return <div>{count}</div>;
370
+ }
371
+
372
+ render(
373
+ <StrictMode>
374
+ <TestComponent />
375
+ </StrictMode>,
376
+ );
377
+
378
+ // useLayoutEffect runs synchronously after render, before paint
379
+ expect(events).toEqual([
380
+ "render count=0",
381
+ "render count=0",
382
+ "layoutEffect count=0",
383
+ "layoutCleanup count=0",
384
+ "layoutEffect count=0",
385
+ "render count=1",
386
+ "render count=1",
387
+ "layoutCleanup count=0",
388
+ "layoutEffect count=1",
389
+ ]);
390
+ });
391
+ });
392
+ });
@@ -0,0 +1,274 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { resource } from "../../core/resource";
3
+ import { isDevelopment } from "../../core/env";
4
+ import { tapRef } from "../../hooks/tap-ref";
5
+ import { tapState } from "../../hooks/tap-state";
6
+ import { tapEffect } from "../../hooks/tap-effect";
7
+ import { tapResource } from "../../hooks/tap-resource";
8
+ import { createResource } from "../../core/createResource";
9
+ import { withKey } from "../../core/withKey";
10
+
11
+ describe("Strict Mode", () => {
12
+ it("should be in development", () => {
13
+ expect(isDevelopment).toBe(true);
14
+ });
15
+
16
+ it("should double-render on first render", () => {
17
+ let renderCount = 0;
18
+
19
+ const TestResource = resource(() => {
20
+ renderCount++;
21
+ return { renderCount };
22
+ });
23
+
24
+ const handle = createResource(TestResource(), { devStrictMode: true });
25
+ const output = handle.getValue();
26
+
27
+ expect(renderCount).toBe(2);
28
+ expect(output.renderCount).toBe(2);
29
+ });
30
+
31
+ it("should double-call hook fns", () => {
32
+ let renderCount = 0;
33
+
34
+ const TestResource = resource(() => {
35
+ const ref = tapRef(0);
36
+ const [count] = tapState(() => {
37
+ renderCount++;
38
+ return ++ref.current;
39
+ });
40
+ const [count2] = tapState(() => {
41
+ renderCount++;
42
+ return ++ref.current;
43
+ });
44
+
45
+ expect(count).toBe(1);
46
+ expect(count2).toBe(3);
47
+ expect(ref.current).toBe(4);
48
+ });
49
+
50
+ createResource(TestResource(), { devStrictMode: true });
51
+
52
+ expect(renderCount).toBe(4);
53
+ });
54
+
55
+ it("should double-commit effects", () => {
56
+ const events: string[] = [];
57
+ const TestResource = resource(() => {
58
+ const ref = tapRef(0);
59
+ ref.current++;
60
+ const count = ref.current;
61
+
62
+ tapEffect(() => {
63
+ events.push("mount-1");
64
+
65
+ return () => {
66
+ events.push("unmount-1");
67
+ };
68
+ });
69
+
70
+ tapEffect(() => {
71
+ events.push("mount-2");
72
+
73
+ return () => {
74
+ events.push("unmount-2");
75
+ };
76
+ }, []);
77
+
78
+ tapEffect(() => {
79
+ expect(count).toBe(2);
80
+
81
+ events.push("mount-3");
82
+
83
+ return () => {
84
+ events.push("unmount-3");
85
+ };
86
+ }, [count]);
87
+ });
88
+
89
+ createResource(TestResource(), { devStrictMode: true });
90
+
91
+ expect(events).toEqual([
92
+ "mount-1",
93
+ "mount-2",
94
+ "mount-3",
95
+ "unmount-1",
96
+ "unmount-2",
97
+ "unmount-3",
98
+ "mount-1",
99
+ "mount-2",
100
+ "mount-3",
101
+ ]);
102
+ });
103
+
104
+ it("should double-render on child render", () => {
105
+ let renderCount = 0;
106
+
107
+ const TestChildResource = resource(() => {
108
+ renderCount++;
109
+ return { renderCount };
110
+ });
111
+
112
+ const TestResource = resource(() => {
113
+ return tapResource(TestChildResource());
114
+ });
115
+
116
+ const handle = createResource(TestResource(), { devStrictMode: true });
117
+ const output = handle.getValue();
118
+
119
+ expect(renderCount).toBe(2);
120
+ expect(output.renderCount).toBe(2);
121
+ });
122
+
123
+ it("should double-mount before handling state updates", () => {
124
+ const events: string[] = [];
125
+ const TestResource = resource(() => {
126
+ const [id, setId] = tapState(0);
127
+ events.push(`render-${id}`);
128
+ tapEffect(() => {
129
+ events.push(`mount-${id}`);
130
+ setId(1);
131
+ return () => {
132
+ events.push(`unmount-${id}`);
133
+ };
134
+ });
135
+ });
136
+
137
+ createResource(TestResource(), {
138
+ mount: true,
139
+ devStrictMode: true,
140
+ });
141
+
142
+ expect(events).toEqual([
143
+ "render-0",
144
+ "render-0",
145
+ "mount-0",
146
+ "unmount-0",
147
+ "mount-0",
148
+ "render-1",
149
+ "render-1",
150
+ "unmount-0",
151
+ "mount-1",
152
+ ]);
153
+ });
154
+
155
+ it("should double-render on child render change", () => {
156
+ let renderCount = 0;
157
+ let fnCount = 0;
158
+ let mountCount = 0;
159
+ let unmountCount = 0;
160
+
161
+ const incrementRenderCount = () => {
162
+ renderCount++;
163
+ return renderCount;
164
+ };
165
+
166
+ const TestChildResource = resource(() => {
167
+ const [fnState] = tapState(() => {
168
+ fnCount++;
169
+ return fnCount;
170
+ });
171
+ const count = incrementRenderCount();
172
+ tapEffect(() => {
173
+ expect(fnState % 2).toBe(1);
174
+ expect(count).toBe(fnState + 1);
175
+
176
+ mountCount++;
177
+ return () => {
178
+ unmountCount++;
179
+ };
180
+ }, [fnState, count]);
181
+ return { renderCount, fnCount, fnState };
182
+ });
183
+
184
+ const TestResource = resource(() => {
185
+ const [id, setId] = tapState(0);
186
+ tapEffect(() => {
187
+ setId(1);
188
+ });
189
+ return tapResource(withKey(id, TestChildResource()));
190
+ });
191
+
192
+ const handle = createResource(TestResource(), {
193
+ mount: true,
194
+ devStrictMode: true,
195
+ });
196
+ const output = handle.getValue();
197
+
198
+ expect(renderCount).toBe(4);
199
+ expect(fnCount).toBe(4);
200
+ expect(output.renderCount).toBe(4);
201
+ expect(output.fnCount).toBe(4);
202
+ expect(output.fnState).toBe(3);
203
+ expect(mountCount).toBe(4);
204
+ expect(unmountCount).toBe(3);
205
+ });
206
+
207
+ it("should double-render on child render change", () => {
208
+ let renderCount = 0;
209
+ const events: string[] = [];
210
+ const TestChildResource = resource(() => {
211
+ renderCount++;
212
+ events.push(`render-${renderCount}`);
213
+
214
+ tapState(() => {
215
+ return events.push(`fn-${renderCount}`);
216
+ });
217
+
218
+ const count = renderCount;
219
+ tapEffect(() => {
220
+ events.push(`mount-${count}`);
221
+ return () => {
222
+ events.push(`unmount-${count}`);
223
+ };
224
+ });
225
+ });
226
+
227
+ const TestResource = resource(() => {
228
+ const [id, setId] = tapState(0);
229
+ events.push(`outer-render-${id}`);
230
+ tapEffect(() => {
231
+ events.push(`outer-mount-${id}`);
232
+ setId(1);
233
+
234
+ return () => {
235
+ events.push(`outer-unmount-${id}`);
236
+ };
237
+ });
238
+ return tapResource(withKey(id, TestChildResource()));
239
+ });
240
+
241
+ createResource(TestResource(), {
242
+ mount: true,
243
+ devStrictMode: true,
244
+ });
245
+
246
+ expect(events).toEqual([
247
+ "outer-render-0",
248
+ "render-1",
249
+ "fn-1",
250
+ "fn-1",
251
+ "outer-render-0",
252
+ "render-2",
253
+ "outer-mount-0",
254
+ "mount-2",
255
+ "outer-unmount-0",
256
+ "unmount-2",
257
+ "outer-mount-0",
258
+ "mount-2",
259
+ "outer-render-1",
260
+ "render-3",
261
+ "fn-3",
262
+ "fn-3",
263
+ "outer-render-1",
264
+ "render-4",
265
+ "outer-unmount-0",
266
+ "outer-mount-1",
267
+ "unmount-2",
268
+ "mount-4",
269
+ "unmount-4",
270
+ "mount-4",
271
+ ]);
272
+ // expect(renderCount).toBe(4);
273
+ });
274
+ });