@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.
Files changed (215) 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 +17 -16
  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.d.ts +8 -10
  110. package/dist/react-shim/index.d.ts.map +1 -1
  111. package/dist/react-shim/index.js +19 -19
  112. package/dist/react-shim/index.js.map +1 -1
  113. package/package.json +1 -1
  114. package/src/__tests__/basic/resourceHandle.test.ts +32 -22
  115. package/src/__tests__/basic/tapEffect.basic.test.ts +8 -8
  116. package/src/__tests__/basic/tapReducer.basic.test.ts +16 -14
  117. package/src/__tests__/basic/tapResources.basic.test.ts +19 -16
  118. package/src/__tests__/basic/tapState.basic.test.ts +11 -11
  119. package/src/__tests__/bench/hosts.bench.tsx +124 -0
  120. package/src/__tests__/bench/tree.bench.tsx +166 -0
  121. package/src/__tests__/errors/errors.effect-errors.test.ts +12 -13
  122. package/src/__tests__/errors/errors.render-errors.test.ts +65 -22
  123. package/src/__tests__/lifecycle/lifecycle.dependencies.test.ts +19 -19
  124. package/src/__tests__/lifecycle/lifecycle.mount-unmount.test.ts +14 -14
  125. package/src/__tests__/parity/describeParity.tsx +217 -0
  126. package/src/__tests__/parity/parity.adversarial.test.tsx +375 -0
  127. package/src/__tests__/parity/parity.basics.test.tsx +281 -0
  128. package/src/__tests__/parity/parity.divergences.test.tsx +208 -0
  129. package/src/__tests__/parity/parity.smoke.test.tsx +43 -0
  130. package/src/__tests__/react/concurrent-mode.test.tsx +10 -6
  131. package/src/__tests__/react/concurrent-pending-updates.test.tsx +351 -0
  132. package/src/__tests__/react/concurrent-render-phase.test.tsx +350 -0
  133. package/src/__tests__/react/react-shim.test.tsx +1 -1
  134. package/src/__tests__/react/useResource.test.tsx +41 -26
  135. package/src/__tests__/react/useTapHost.test.tsx +233 -0
  136. package/src/__tests__/react-dispatcher.test.ts +4 -4
  137. package/src/__tests__/rules/rules.hook-count.test.ts +21 -21
  138. package/src/__tests__/rules/rules.hook-order.test.ts +17 -17
  139. package/src/__tests__/strictmode/strictmode-parity.test.tsx +420 -0
  140. package/src/__tests__/strictmode/strictmode.test.ts +39 -209
  141. package/src/__tests__/test-utils.ts +33 -23
  142. package/src/core/ResourceFiber.ts +43 -35
  143. package/src/core/createTapRoot.ts +45 -0
  144. package/src/core/helpers/commit.ts +12 -2
  145. package/src/core/helpers/execution-context.ts +4 -13
  146. package/src/core/helpers/root.ts +24 -12
  147. package/src/core/react-dispatcher.ts +14 -13
  148. package/src/core/resource.ts +5 -20
  149. package/src/core/scheduler.ts +1 -1
  150. package/src/core/types.ts +27 -21
  151. package/src/hooks/useResource.ts +18 -27
  152. package/src/hooks/useResources.ts +18 -42
  153. package/src/hooks/useTapHost.ts +60 -0
  154. package/src/hooks/useTapRoot.ts +135 -0
  155. package/src/hooks/utils/depsShallowEqual.ts +12 -2
  156. package/src/hooks/utils/useCell.ts +2 -2
  157. package/src/hooks/utils/useDevStrictMode.ts +34 -0
  158. package/src/hooks/utils/useRenderMemo.ts +27 -0
  159. package/src/hooks/utils/useResourceFiberHostUtils.ts +61 -0
  160. package/src/index.ts +6 -3
  161. package/src/{hooks → react-hooks}/index.ts +4 -4
  162. package/src/react-hooks/useEffect.ts +58 -0
  163. package/src/{hooks → react-hooks}/useMemo.ts +1 -1
  164. package/src/react-hooks/useReducer.ts +254 -0
  165. package/src/{hooks → react-hooks}/useState.ts +2 -2
  166. package/src/react-shim/index.ts +24 -13
  167. package/dist/core/createResourceRoot.d.ts +0 -11
  168. package/dist/core/createResourceRoot.d.ts.map +0 -1
  169. package/dist/core/createResourceRoot.js +0 -31
  170. package/dist/core/createResourceRoot.js.map +0 -1
  171. package/dist/core/helpers/callResourceFn.d.ts +0 -1
  172. package/dist/core/helpers/callResourceFn.js +0 -19
  173. package/dist/core/helpers/callResourceFn.js.map +0 -1
  174. package/dist/hooks/use.js.map +0 -1
  175. package/dist/hooks/useCallback.d.ts.map +0 -1
  176. package/dist/hooks/useCallback.js.map +0 -1
  177. package/dist/hooks/useEffect.d.ts.map +0 -1
  178. package/dist/hooks/useEffect.js +0 -40
  179. package/dist/hooks/useEffect.js.map +0 -1
  180. package/dist/hooks/useEffectEvent.d.ts.map +0 -1
  181. package/dist/hooks/useEffectEvent.js.map +0 -1
  182. package/dist/hooks/useMemo.d.ts.map +0 -1
  183. package/dist/hooks/useMemo.js.map +0 -1
  184. package/dist/hooks/useMemoCache.d.ts.map +0 -1
  185. package/dist/hooks/useMemoCache.js.map +0 -1
  186. package/dist/hooks/useReducer.d.ts +0 -21
  187. package/dist/hooks/useReducer.d.ts.map +0 -1
  188. package/dist/hooks/useReducer.js +0 -81
  189. package/dist/hooks/useReducer.js.map +0 -1
  190. package/dist/hooks/useRef.d.ts.map +0 -1
  191. package/dist/hooks/useRef.js.map +0 -1
  192. package/dist/hooks/useResourceRoot.d.ts +0 -20
  193. package/dist/hooks/useResourceRoot.d.ts.map +0 -1
  194. package/dist/hooks/useResourceRoot.js +0 -77
  195. package/dist/hooks/useResourceRoot.js.map +0 -1
  196. package/dist/hooks/useState.d.ts.map +0 -1
  197. package/dist/hooks/useState.js.map +0 -1
  198. package/dist/react/hooks.d.ts +0 -25
  199. package/dist/react/hooks.d.ts.map +0 -1
  200. package/dist/react/hooks.js +0 -69
  201. package/dist/react/hooks.js.map +0 -1
  202. package/src/__tests__/strictmode/react-strictmode-behavior.test.tsx +0 -920
  203. package/src/__tests__/strictmode/react-strictmode-rerender-sources.test.tsx +0 -488
  204. package/src/__tests__/strictmode/tap-strictmode-rerender-sources.test.ts +0 -687
  205. package/src/core/createResourceRoot.ts +0 -53
  206. package/src/core/helpers/callResourceFn.ts +0 -21
  207. package/src/hooks/useEffect.ts +0 -72
  208. package/src/hooks/useReducer.ts +0 -160
  209. package/src/hooks/useResourceRoot.ts +0 -130
  210. package/src/react/hooks.ts +0 -112
  211. /package/src/{hooks → react-hooks}/use.ts +0 -0
  212. /package/src/{hooks → react-hooks}/useCallback.ts +0 -0
  213. /package/src/{hooks → react-hooks}/useEffectEvent.ts +0 -0
  214. /package/src/{hooks → react-hooks}/useMemoCache.ts +0 -0
  215. /package/src/{hooks → react-hooks}/useRef.ts +0 -0
@@ -8,13 +8,13 @@ import {
8
8
  import { resource } from "../../core/resource";
9
9
  import { withKey } from "../../core/withKey";
10
10
  import { useState } from "react";
11
- import { useState as useResourceState } from "../../hooks/useState";
12
- import { useEffect as useResourceEffect } from "../../hooks/useEffect";
11
+ import { useState as useResourceState } from "../../react-hooks/useState";
12
+ import { useEffect as useResourceEffect } from "../../react-hooks/useEffect";
13
13
  import {
14
14
  useResource,
15
15
  useResources,
16
- useResourceRoot,
17
- flushResourcesSync,
16
+ useTapRoot,
17
+ flushTapSync,
18
18
  } from "../../index";
19
19
 
20
20
  describe("@assistant-ui/tap/react resource API", () => {
@@ -25,18 +25,22 @@ describe("@assistant-ui/tap/react resource API", () => {
25
25
 
26
26
  describe("useResource", () => {
27
27
  it("routes to useResource inside a tap resource", () => {
28
- const Child = resource(function Child(props: { n: number }) {
28
+ const useChild = (props: { n: number }) => {
29
29
  return props.n * 2;
30
- });
30
+ };
31
+
32
+ const Child = resource(useChild);
31
33
  const parent = createTestResource(() => useResource(Child({ n: 21 })));
32
- expect(renderTest(parent, undefined)).toBe(42);
34
+ expect(renderTest(parent)).toBe(42);
33
35
  });
34
36
 
35
37
  it("routes to the React bridge inside a component", () => {
36
- const CounterResource = resource(function CounterResource() {
38
+ const useCounterResource = () => {
37
39
  const [count, setCount] = useResourceState(0);
38
40
  return { count, setCount };
39
- });
41
+ };
42
+
43
+ const CounterResource = resource(useCounterResource);
40
44
 
41
45
  let api: { count: number; setCount: (n: number) => void } | null = null;
42
46
  function App() {
@@ -53,23 +57,27 @@ describe("@assistant-ui/tap/react resource API", () => {
53
57
 
54
58
  describe("useResources", () => {
55
59
  it("hosts a keyed list inside a tap resource", () => {
56
- const Item = resource(function Item(p: { n: number }) {
60
+ const useItem = (p: { n: number }) => {
57
61
  return p.n * 10;
58
- });
62
+ };
63
+
64
+ const Item = resource(useItem);
59
65
  const parent = createTestResource(() =>
60
66
  useResources(() => [
61
67
  withKey("a", Item({ n: 1 })),
62
68
  withKey("b", Item({ n: 2 })),
63
69
  ]),
64
70
  );
65
- expect(renderTest(parent, undefined)).toEqual([10, 20]);
71
+ expect(renderTest(parent)).toEqual([10, 20]);
66
72
  });
67
73
 
68
74
  it("hosts a keyed list inside a React component and tracks deps", () => {
69
- const Item = resource(function Item(p: { n: number }) {
75
+ const useItem = (p: { n: number }) => {
70
76
  const [v] = useResourceState(p.n * 10);
71
77
  return v;
72
- });
78
+ };
79
+
80
+ const Item = resource(useItem);
73
81
 
74
82
  let setCount: (n: number) => void = () => {};
75
83
  function App() {
@@ -92,16 +100,19 @@ describe("@assistant-ui/tap/react resource API", () => {
92
100
  });
93
101
  });
94
102
 
95
- describe("useResourceRoot", () => {
103
+ describe("useTapRoot", () => {
96
104
  it("exposes a subscribable inside a tap resource", () => {
97
- const Root = resource(function Root() {
105
+ const useRoot = () => {
98
106
  const [n] = useResourceState(7);
99
107
  return n;
100
- });
108
+ };
109
+
101
110
  const parent = createTestResource(() =>
102
- useResourceRoot(Root()).getValue(),
111
+ useTapRoot(function Root() {
112
+ return useRoot();
113
+ }).getValue(),
103
114
  );
104
- expect(renderTest(parent, undefined)).toBe(7);
115
+ expect(renderTest(parent)).toBe(7);
105
116
  });
106
117
 
107
118
  // A root is push-based: host it in one place and observe it via getValue/
@@ -110,19 +121,21 @@ describe("@assistant-ui/tap/react resource API", () => {
110
121
  // and the root notifies on output change — so this test observes the store
111
122
  // directly rather than through a same-component useSyncExternalStore.)
112
123
  it("hosts a subscribable root inside a React component", () => {
113
- const CounterRoot = resource(function CounterRoot() {
124
+ const useCounterRoot = () => {
114
125
  const [count, setCount] = useResourceState(0);
115
126
  return { count, setCount };
116
- });
127
+ };
117
128
 
118
129
  let store: ReturnType<
119
- typeof useResourceRoot<{
130
+ typeof useTapRoot<{
120
131
  count: number;
121
132
  setCount: (n: number) => void;
122
133
  }>
123
134
  > | null = null;
124
135
  function App() {
125
- store = useResourceRoot(CounterRoot());
136
+ store = useTapRoot(function Root() {
137
+ return useCounterRoot();
138
+ });
126
139
  return null;
127
140
  }
128
141
 
@@ -136,7 +149,7 @@ describe("@assistant-ui/tap/react resource API", () => {
136
149
 
137
150
  // The root drives updates through tap's own (macrotask) scheduler, so flush
138
151
  // synchronously to observe.
139
- flushResourcesSync(() => store!.getValue().setCount(5));
152
+ flushTapSync(() => store!.getValue().setCount(5));
140
153
  expect(store!.getValue().count).toBe(5);
141
154
  expect(notified).toBeGreaterThan(0);
142
155
 
@@ -147,11 +160,13 @@ describe("@assistant-ui/tap/react resource API", () => {
147
160
  describe("useResource key remount (React bridge)", () => {
148
161
  it("remounts the hosted resource when the element key changes", () => {
149
162
  const mounts: number[] = [];
150
- const Keyed = resource(function Keyed(p: { id: number }) {
163
+ const useKeyed = (p: { id: number }) => {
151
164
  // oxlint-disable-next-line react/exhaustive-deps -- capture the mount id once per fiber to assert remount on key change
152
165
  useResourceEffect(() => void mounts.push(p.id), []);
153
166
  return p.id;
154
- });
167
+ };
168
+
169
+ const Keyed = resource(useKeyed);
155
170
 
156
171
  let setId: (n: number) => void = () => {};
157
172
  function App() {
@@ -0,0 +1,233 @@
1
+ import { describe, it, expect, afterEach } from "vitest";
2
+ import { render, screen, act, cleanup } from "@testing-library/react";
3
+ import { StrictMode, useEffect, useLayoutEffect } from "react";
4
+ import { useState as useTapState } from "../../react-hooks/useState";
5
+ import { useEffect as useTapEffect } from "../../react-hooks/useEffect";
6
+ import { useTapHost } from "../../index";
7
+
8
+ const countOf = (log: string[], entry: string) =>
9
+ log.filter((e) => e === entry).length;
10
+
11
+ describe("useTapHost", () => {
12
+ afterEach(() => {
13
+ cleanup();
14
+ });
15
+
16
+ it("returns the resource value and re-renders on dispatch", () => {
17
+ let api!: { count: number; setCount: (n: number) => void };
18
+ function App() {
19
+ const { value } = useTapHost(function TapHost() {
20
+ const [count, setCount] = useTapState(0);
21
+ return { count, setCount };
22
+ });
23
+ api = value;
24
+ return <div data-testid="count">{value.count}</div>;
25
+ }
26
+
27
+ render(<App />);
28
+ expect(screen.getByTestId("count").textContent).toBe("0");
29
+ act(() => api.setCount(3));
30
+ expect(screen.getByTestId("count").textContent).toBe("3");
31
+ });
32
+
33
+ it("commits in the passive phase, before the effects of a consumer that mounts effects", () => {
34
+ const log: string[] = [];
35
+ function Child({ effects }: { effects: () => void }) {
36
+ useEffect(effects);
37
+ useLayoutEffect(() => {
38
+ log.push("child layout");
39
+ }, []);
40
+ useEffect(() => {
41
+ log.push("child effect");
42
+ }, []);
43
+ return null;
44
+ }
45
+ function App() {
46
+ const { effects } = useTapHost(function TapHost() {
47
+ useTapEffect(() => {
48
+ log.push("tap effect");
49
+ });
50
+ return null;
51
+ });
52
+ useEffect(() => {
53
+ log.push("host effect");
54
+ }, []);
55
+ return <Child effects={effects} />;
56
+ }
57
+
58
+ render(<App />);
59
+ // "child layout" first proves the commit is passive (does not block
60
+ // paint); "tap effect" before "child effect" proves the consumer's
61
+ // instance won the commit over the host's own fallback instance.
62
+ expect(log).toEqual([
63
+ "child layout",
64
+ "tap effect",
65
+ "child effect",
66
+ "host effect",
67
+ ]);
68
+ });
69
+
70
+ it("falls back to the host's own instance when no consumer mounts effects", () => {
71
+ const log: string[] = [];
72
+ function Child() {
73
+ useEffect(() => {
74
+ log.push("child effect");
75
+ }, []);
76
+ return null;
77
+ }
78
+ function App() {
79
+ useTapHost(function TapHost() {
80
+ useTapEffect(() => {
81
+ log.push("tap effect");
82
+ });
83
+ return null;
84
+ });
85
+ return <Child />;
86
+ }
87
+
88
+ render(<App />);
89
+ expect(log).toEqual(["child effect", "tap effect"]);
90
+ });
91
+
92
+ it("commits exactly once per pass with multiple consumers", () => {
93
+ const log: string[] = [];
94
+ let api!: { bump: () => void };
95
+ function Child({ effects }: { effects: () => void }) {
96
+ useEffect(effects);
97
+ return null;
98
+ }
99
+ function App() {
100
+ const { value, effects } = useTapHost(function TapHost() {
101
+ const [count, setCount] = useTapState(0);
102
+ useTapEffect(() => {
103
+ log.push("tap effect");
104
+ });
105
+ return { count, bump: () => setCount(count + 1) };
106
+ });
107
+ api = value;
108
+ return (
109
+ <>
110
+ <Child effects={effects} />
111
+ <Child effects={effects} />
112
+ </>
113
+ );
114
+ }
115
+
116
+ render(<App />);
117
+ expect(countOf(log, "tap effect")).toBe(1);
118
+
119
+ act(() => api.bump());
120
+ expect(countOf(log, "tap effect")).toBe(2);
121
+ });
122
+
123
+ it("hands responsibility to the next instance when the winner unmounts", () => {
124
+ const log: string[] = [];
125
+ let api!: { count: number; bump: () => void };
126
+ function Child({ name, effects }: { name: string; effects: () => void }) {
127
+ useEffect(effects);
128
+ useEffect(() => {
129
+ log.push(`${name} effect`);
130
+ });
131
+ return null;
132
+ }
133
+ function App({ showA }: { showA: boolean }) {
134
+ const { value, effects } = useTapHost(function TapHost() {
135
+ const [count, setCount] = useTapState(0);
136
+ useTapEffect(() => {
137
+ log.push(`tap effect ${count}`);
138
+ return () => {
139
+ log.push("tap cleanup");
140
+ };
141
+ });
142
+ return { count, bump: () => setCount(count + 1) };
143
+ });
144
+ api = value;
145
+ return (
146
+ <>
147
+ {showA && <Child name="a" effects={effects} />}
148
+ <Child name="b" effects={effects} />
149
+ </>
150
+ );
151
+ }
152
+
153
+ const { rerender } = render(<App showA={true} />);
154
+ expect(log).toEqual(["tap effect 0", "a effect", "b effect"]);
155
+
156
+ log.length = 0;
157
+ rerender(<App showA={false} />);
158
+ // The winner's unmount removes only its instance, not the resource: the
159
+ // no-deps tap effect re-fires as a cleanup/setup pair, not a final
160
+ // cleanup, and the commit still lands before b's own effect.
161
+ expect(log).toEqual(["tap cleanup", "tap effect 0", "b effect"]);
162
+
163
+ log.length = 0;
164
+ act(() => api.bump());
165
+ expect(api.count).toBe(1);
166
+ // Next in line (b) now commits, still ahead of its own effects.
167
+ expect(log).toEqual(["tap cleanup", "tap effect 1", "b effect"]);
168
+ });
169
+
170
+ it("unmounting the host cleans up exactly once", () => {
171
+ // effects consumers must be descendants of the host component, so
172
+ // they unmount with it and no flush can fire after the deletion.
173
+ const log: string[] = [];
174
+ function Child({ effects }: { effects: () => void }) {
175
+ useEffect(effects);
176
+ return null;
177
+ }
178
+ function HostComp() {
179
+ const { effects } = useTapHost(function TapHost() {
180
+ useTapEffect(() => {
181
+ log.push("tap effect");
182
+ return () => {
183
+ log.push("tap cleanup");
184
+ };
185
+ }, []);
186
+ return null;
187
+ });
188
+ return <Child effects={effects} />;
189
+ }
190
+ function App({ showHost }: { showHost: boolean }) {
191
+ return showHost ? <HostComp /> : null;
192
+ }
193
+
194
+ const { rerender } = render(<App showHost={true} />);
195
+ expect(log).toEqual(["tap effect"]);
196
+
197
+ rerender(<App showHost={false} />);
198
+ expect(log).toEqual(["tap effect", "tap cleanup"]);
199
+ });
200
+
201
+ it("remounts through a StrictMode effect cycle", () => {
202
+ const log: string[] = [];
203
+ let api!: { count: number; setCount: (n: number) => void };
204
+ function App() {
205
+ const { value } = useTapHost(function TapHost() {
206
+ const [count, setCount] = useTapState(0);
207
+ useTapEffect(() => {
208
+ log.push("tap mount");
209
+ return () => {
210
+ log.push("tap unmount");
211
+ };
212
+ }, []);
213
+ return { count, setCount };
214
+ });
215
+ api = value;
216
+ return <div data-testid="count">{value.count}</div>;
217
+ }
218
+
219
+ render(
220
+ <StrictMode>
221
+ <App />
222
+ </StrictMode>,
223
+ );
224
+ // setup, simulated unmount, setup: same as inlined hooks in StrictMode.
225
+ expect(countOf(log, "tap mount")).toBe(2);
226
+ expect(countOf(log, "tap unmount")).toBe(1);
227
+
228
+ act(() => api.setCount(5));
229
+ expect(screen.getByTestId("count").textContent).toBe("5");
230
+ expect(countOf(log, "tap mount")).toBe(2);
231
+ expect(countOf(log, "tap unmount")).toBe(1);
232
+ });
233
+ });
@@ -28,7 +28,7 @@ describe("react dispatcher", () => {
28
28
  return n;
29
29
  });
30
30
 
31
- expect(renderTest(fiber, undefined)).toBe(10);
31
+ expect(renderTest(fiber)).toBe(10);
32
32
  set(42);
33
33
  await waitForNextTick();
34
34
  expect(getCommittedOutput(fiber)).toBe(42);
@@ -60,15 +60,15 @@ describe("react dispatcher", () => {
60
60
  return $[0];
61
61
  });
62
62
 
63
- expect(renderTest(fiber, undefined)).toBe("computed-once");
63
+ expect(renderTest(fiber)).toBe("computed-once");
64
64
  // re-render: the cache persists across renders, slot already filled
65
- expect(renderTest(fiber, undefined)).toBe("computed-once");
65
+ expect(renderTest(fiber)).toBe("computed-once");
66
66
  });
67
67
 
68
68
  it("throws for a hook tap does not implement", () => {
69
69
  const fiber = createTestResource(() => React.useId());
70
70
  // render directly: a mid-render throw must not leave a tracked, unmounted
71
71
  // fiber for `cleanupAllResources` to choke on.
72
- expect(() => renderResourceFiber(fiber, undefined)).toThrow();
72
+ expect(() => renderResourceFiber(fiber, [])).toThrow();
73
73
  });
74
74
  });
@@ -1,7 +1,7 @@
1
1
  /* oxlint-disable react/rules-of-hooks -- tests deliberately exercise conditional/nested hook patterns */
2
2
  import { describe, it, expect } from "vitest";
3
- import { useEffect } from "../../hooks/useEffect";
4
- import { useState } from "../../hooks/useState";
3
+ import { useEffect } from "../../react-hooks/useEffect";
4
+ import { useState } from "../../react-hooks/useState";
5
5
  import { createTestResource, renderTest } from "../test-utils";
6
6
  import { renderResourceFiber } from "../../core/ResourceFiber";
7
7
 
@@ -18,11 +18,11 @@ describe("Rules of Hooks - Hook Count", () => {
18
18
  });
19
19
 
20
20
  // First render establishes 5 hooks
21
- renderTest(resource, undefined);
21
+ renderTest(resource);
22
22
 
23
23
  // Second render should work with same count
24
24
  expect(() => {
25
- renderTest(resource, undefined);
25
+ renderTest(resource);
26
26
  }).not.toThrow();
27
27
  });
28
28
 
@@ -41,12 +41,12 @@ describe("Rules of Hooks - Hook Count", () => {
41
41
  });
42
42
 
43
43
  // First render with 2 hooks
44
- renderResourceFiber(resource, undefined);
44
+ renderResourceFiber(resource, []);
45
45
 
46
46
  // Try to render with 3 hooks
47
47
  addExtraHook = true;
48
48
 
49
- expect(() => renderResourceFiber(resource, undefined)).toThrow(
49
+ expect(() => renderResourceFiber(resource, [])).toThrow(
50
50
  "Rendered more hooks than during the previous render",
51
51
  );
52
52
  });
@@ -66,12 +66,12 @@ describe("Rules of Hooks - Hook Count", () => {
66
66
  });
67
67
 
68
68
  // First render with 3 hooks
69
- renderResourceFiber(resource, undefined);
69
+ renderResourceFiber(resource, []);
70
70
 
71
71
  // Try to render with 2 hooks
72
72
  skipHook = true;
73
73
 
74
- expect(() => renderResourceFiber(resource, undefined)).toThrow(
74
+ expect(() => renderResourceFiber(resource, [])).toThrow(
75
75
  "Rendered 2 hooks but expected 3",
76
76
  );
77
77
  });
@@ -89,11 +89,11 @@ describe("Rules of Hooks - Hook Count", () => {
89
89
  return null;
90
90
  });
91
91
 
92
- renderResourceFiber(resource, undefined);
92
+ renderResourceFiber(resource, []);
93
93
 
94
94
  includeEffect = false;
95
95
 
96
- expect(() => renderResourceFiber(resource, undefined)).toThrow(
96
+ expect(() => renderResourceFiber(resource, [])).toThrow(
97
97
  "Rendered 2 hooks but expected 3",
98
98
  );
99
99
  });
@@ -104,10 +104,10 @@ describe("Rules of Hooks - Hook Count", () => {
104
104
  return "no hooks";
105
105
  });
106
106
 
107
- renderTest(resource, undefined);
107
+ renderTest(resource);
108
108
 
109
109
  // Should allow multiple renders with zero hooks
110
- expect(() => renderTest(resource, undefined)).not.toThrow();
110
+ expect(() => renderTest(resource)).not.toThrow();
111
111
  });
112
112
 
113
113
  it("should detect dynamic hook creation", () => {
@@ -120,12 +120,12 @@ describe("Rules of Hooks - Hook Count", () => {
120
120
  return null;
121
121
  });
122
122
 
123
- renderResourceFiber(resource, undefined);
123
+ renderResourceFiber(resource, []);
124
124
 
125
125
  // Change hook count
126
126
  hookCount = 3;
127
127
 
128
- expect(() => renderResourceFiber(resource, undefined)).toThrow(
128
+ expect(() => renderResourceFiber(resource, [])).toThrow(
129
129
  "Rendered more hooks than during the previous render",
130
130
  );
131
131
  });
@@ -144,7 +144,7 @@ describe("Rules of Hooks - Hook Count", () => {
144
144
 
145
145
  // Multiple renders should all maintain same hook count
146
146
  for (let i = 0; i < 5; i++) {
147
- expect(() => renderTest(resource, undefined)).not.toThrow();
147
+ expect(() => renderTest(resource)).not.toThrow();
148
148
  }
149
149
 
150
150
  expect(renderCount).toBe(5);
@@ -166,12 +166,12 @@ describe("Rules of Hooks - Hook Count", () => {
166
166
  });
167
167
 
168
168
  // Render both
169
- renderTest(resource1, undefined);
170
- renderTest(resource2, undefined);
169
+ renderTest(resource1);
170
+ renderTest(resource2);
171
171
 
172
172
  // Each should maintain its own count
173
- expect(() => renderTest(resource1, undefined)).not.toThrow();
174
- expect(() => renderTest(resource2, undefined)).not.toThrow();
173
+ expect(() => renderTest(resource1)).not.toThrow();
174
+ expect(() => renderTest(resource2)).not.toThrow();
175
175
  });
176
176
 
177
177
  it("should detect hook count changes in nested function calls", () => {
@@ -190,11 +190,11 @@ describe("Rules of Hooks - Hook Count", () => {
190
190
  return null;
191
191
  });
192
192
 
193
- renderResourceFiber(resource, undefined);
193
+ renderResourceFiber(resource, []);
194
194
 
195
195
  useExtraHooks = true;
196
196
 
197
- expect(() => renderResourceFiber(resource, undefined)).toThrow(
197
+ expect(() => renderResourceFiber(resource, [])).toThrow(
198
198
  "Rendered more hooks than during the previous render",
199
199
  );
200
200
  });
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import { useEffect } from "../../hooks/useEffect";
3
- import { useState } from "../../hooks/useState";
2
+ import { useEffect } from "../../react-hooks/useEffect";
3
+ import { useState } from "../../react-hooks/useState";
4
4
  import { createTestResource, renderTest } from "../test-utils";
5
5
  import {
6
6
  renderResourceFiber,
@@ -23,13 +23,13 @@ describe("Rules of Hooks - Hook Order", () => {
23
23
  });
24
24
 
25
25
  // First render establishes order
26
- renderTest(resource, undefined);
26
+ renderTest(resource);
27
27
 
28
28
  // Change condition
29
29
  condition = false;
30
30
 
31
31
  // Second render with different order should throw
32
- expect(() => renderResourceFiber(resource, undefined)).toThrow(
32
+ expect(() => renderResourceFiber(resource, [])).toThrow(
33
33
  "Hook order changed between renders",
34
34
  );
35
35
  });
@@ -46,12 +46,12 @@ describe("Rules of Hooks - Hook Order", () => {
46
46
  return null;
47
47
  });
48
48
 
49
- renderTest(resource, undefined);
49
+ renderTest(resource);
50
50
 
51
51
  // Change to use different hook type
52
52
  addEffect = true;
53
53
 
54
- expect(() => renderResourceFiber(resource, undefined)).toThrow(
54
+ expect(() => renderResourceFiber(resource, [])).toThrow(
55
55
  "Hook order changed between renders",
56
56
  );
57
57
  });
@@ -70,13 +70,13 @@ describe("Rules of Hooks - Hook Order", () => {
70
70
  return null;
71
71
  });
72
72
 
73
- renderTest(resource, undefined);
73
+ renderTest(resource);
74
74
 
75
75
  // Change condition
76
76
  condition = false;
77
77
 
78
78
  // Should throw because hook count changed
79
- expect(() => renderResourceFiber(resource, undefined)).toThrow(
79
+ expect(() => renderResourceFiber(resource, [])).toThrow(
80
80
  "Rendered 2 hooks but expected 3",
81
81
  );
82
82
  });
@@ -93,11 +93,11 @@ describe("Rules of Hooks - Hook Order", () => {
93
93
  return states;
94
94
  });
95
95
 
96
- const result = renderTest(resource, undefined);
96
+ const result = renderTest(resource);
97
97
  expect(result).toEqual([1, 2, 3]);
98
98
 
99
99
  // Re-render should work fine
100
- expect(() => renderResourceFiber(resource, undefined)).not.toThrow();
100
+ expect(() => renderResourceFiber(resource, [])).not.toThrow();
101
101
  });
102
102
 
103
103
  it("should throw when hooks in loops have inconsistent count", () => {
@@ -110,12 +110,12 @@ describe("Rules of Hooks - Hook Order", () => {
110
110
  return null;
111
111
  });
112
112
 
113
- renderTest(resource, undefined);
113
+ renderTest(resource);
114
114
 
115
115
  // Change array length
116
116
  items = [1, 2];
117
117
 
118
- expect(() => renderResourceFiber(resource, undefined)).toThrow(
118
+ expect(() => renderResourceFiber(resource, [])).toThrow(
119
119
  "Rendered 2 hooks but expected 3",
120
120
  );
121
121
  });
@@ -131,11 +131,11 @@ describe("Rules of Hooks - Hook Order", () => {
131
131
  return { a, b, c };
132
132
  });
133
133
 
134
- const result = renderTest(resource, undefined);
134
+ const result = renderTest(resource);
135
135
  expect(result).toEqual({ a: 1, b: 2, c: 3 });
136
136
 
137
137
  // Re-render should maintain same order
138
- const ctx = renderResourceFiber(resource, undefined);
138
+ const ctx = renderResourceFiber(resource, []);
139
139
  expect(() => commitResourceFiber(resource, ctx)).not.toThrow();
140
140
  });
141
141
 
@@ -153,13 +153,13 @@ describe("Rules of Hooks - Hook Order", () => {
153
153
  return a + b;
154
154
  });
155
155
 
156
- const result1 = renderTest(resource, undefined);
156
+ const result1 = renderTest(resource);
157
157
  expect(result1).toBe(3);
158
158
 
159
159
  // Enable early return
160
160
  shouldReturn = true;
161
161
 
162
- expect(() => renderResourceFiber(resource, undefined)).toThrow(
162
+ expect(() => renderResourceFiber(resource, [])).toThrow(
163
163
  "Rendered 1 hooks but expected 2",
164
164
  );
165
165
  });
@@ -187,6 +187,6 @@ describe("Rules of Hooks - Hook Order", () => {
187
187
  return count;
188
188
  });
189
189
 
190
- renderTest(resource, undefined);
190
+ renderTest(resource);
191
191
  });
192
192
  });