@assistant-ui/tap 0.5.14 → 0.6.0
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 -8
- package/dist/core/ResourceFiber.d.ts.map +1 -1
- package/dist/core/ResourceFiber.js +3 -2
- package/dist/core/ResourceFiber.js.map +1 -1
- package/dist/core/context.d.ts +13 -6
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/context.js +19 -6
- package/dist/core/context.js.map +1 -1
- package/dist/core/createResourceRoot.d.ts +2 -1
- package/dist/core/createResourceRoot.d.ts.map +1 -1
- package/dist/core/createResourceRoot.js +2 -2
- package/dist/core/createResourceRoot.js.map +1 -1
- package/dist/core/helpers/execution-context.d.ts +2 -1
- package/dist/core/helpers/execution-context.d.ts.map +1 -1
- package/dist/core/helpers/execution-context.js +4 -1
- package/dist/core/helpers/execution-context.js.map +1 -1
- package/dist/core/react-dispatcher.d.ts +12 -0
- package/dist/core/react-dispatcher.d.ts.map +1 -0
- package/dist/core/react-dispatcher.js +62 -0
- package/dist/core/react-dispatcher.js.map +1 -0
- package/dist/core/scheduler.js +1 -1
- package/dist/core/scheduler.js.map +1 -1
- package/dist/core/types.d.ts +3 -3
- package/dist/hooks/index.d.ts +13 -0
- package/dist/hooks/index.js +13 -0
- package/dist/hooks/use.d.ts +9 -0
- package/dist/hooks/use.d.ts.map +1 -0
- package/dist/hooks/use.js +14 -0
- package/dist/hooks/use.js.map +1 -0
- package/dist/hooks/useCallback.d.ts +5 -0
- package/dist/hooks/useCallback.d.ts.map +1 -0
- package/dist/hooks/useCallback.js +9 -0
- package/dist/hooks/useCallback.js.map +1 -0
- package/dist/hooks/useEffect.d.ts +10 -0
- package/dist/hooks/useEffect.d.ts.map +1 -0
- package/dist/hooks/{tap-effect.js → useEffect.js} +7 -7
- package/dist/hooks/useEffect.js.map +1 -0
- package/dist/hooks/{tap-effect-event.d.ts → useEffectEvent.d.ts} +5 -5
- package/dist/hooks/useEffectEvent.d.ts.map +1 -0
- package/dist/hooks/{tap-effect-event.js → useEffectEvent.js} +12 -12
- package/dist/hooks/useEffectEvent.js.map +1 -0
- package/dist/hooks/useMemo.d.ts +5 -0
- package/dist/hooks/useMemo.d.ts.map +1 -0
- package/dist/hooks/{tap-memo.js → useMemo.js} +6 -6
- package/dist/hooks/useMemo.js.map +1 -0
- package/dist/hooks/useMemoCache.d.ts +10 -0
- package/dist/hooks/useMemoCache.d.ts.map +1 -0
- package/dist/hooks/useMemoCache.js +21 -0
- package/dist/hooks/useMemoCache.js.map +1 -0
- package/dist/hooks/useReducer.d.ts +21 -0
- package/dist/hooks/useReducer.d.ts.map +1 -0
- package/dist/hooks/{tap-reducer.js → useReducer.js} +10 -10
- package/dist/hooks/useReducer.js.map +1 -0
- package/dist/hooks/useRef.d.ts +11 -0
- package/dist/hooks/useRef.d.ts.map +1 -0
- package/dist/hooks/useRef.js +10 -0
- package/dist/hooks/useRef.js.map +1 -0
- package/dist/{react/use-resource.d.ts → hooks/useResource.d.ts} +3 -2
- package/dist/hooks/useResource.d.ts.map +1 -0
- package/dist/hooks/{tap-resource.js → useResource.js} +12 -12
- package/dist/hooks/useResource.js.map +1 -0
- package/dist/hooks/useResourceRoot.d.ts +20 -0
- package/dist/hooks/useResourceRoot.d.ts.map +1 -0
- package/dist/{tapResourceRoot.js → hooks/useResourceRoot.js} +30 -26
- package/dist/hooks/useResourceRoot.js.map +1 -0
- package/dist/hooks/{tap-resources.d.ts → useResources.d.ts} +4 -4
- package/dist/hooks/useResources.d.ts.map +1 -0
- package/dist/hooks/{tap-resources.js → useResources.js} +28 -23
- package/dist/hooks/useResources.js.map +1 -0
- package/dist/hooks/useState.d.ts +9 -0
- package/dist/hooks/useState.d.ts.map +1 -0
- package/dist/hooks/useState.js +11 -0
- package/dist/hooks/useState.js.map +1 -0
- package/dist/hooks/utils/useCell.d.ts +10 -0
- package/dist/hooks/utils/useCell.d.ts.map +1 -0
- package/dist/hooks/utils/{tapHook.js → useCell.js} +4 -4
- package/dist/hooks/utils/{tapHook.js.map → useCell.js.map} +1 -1
- package/dist/index.d.ts +3 -13
- package/dist/index.js +3 -13
- package/dist/react/hooks.d.ts +25 -0
- package/dist/react/hooks.d.ts.map +1 -0
- package/dist/react/hooks.js +69 -0
- package/dist/react/hooks.js.map +1 -0
- package/dist/react-shim/index.d.ts +19 -0
- package/dist/react-shim/index.d.ts.map +1 -0
- package/dist/react-shim/index.js +28 -0
- package/dist/react-shim/index.js.map +1 -0
- package/package.json +13 -16
- package/react-shim/package.json +4 -0
- package/src/__tests__/basic/resourceHandle.test.ts +7 -3
- package/src/__tests__/basic/tapEffect.basic.test.ts +19 -19
- package/src/__tests__/basic/tapReducer.basic.test.ts +14 -14
- package/src/__tests__/basic/tapResources.basic.test.ts +19 -14
- package/src/__tests__/basic/tapState.basic.test.ts +20 -20
- package/src/__tests__/errors/errors.effect-errors.test.ts +21 -21
- package/src/__tests__/errors/errors.render-errors.test.ts +18 -18
- package/src/__tests__/lifecycle/lifecycle.dependencies.test.ts +25 -25
- package/src/__tests__/lifecycle/lifecycle.mount-unmount.test.ts +17 -18
- package/src/__tests__/react/concurrent-mode.test.tsx +7 -7
- package/src/__tests__/react/react-shim.test.tsx +65 -0
- package/src/__tests__/react/useResource.test.tsx +172 -0
- package/src/__tests__/react-dispatcher.test.ts +74 -0
- package/src/__tests__/rules/rules.hook-count.test.ts +30 -29
- package/src/__tests__/rules/rules.hook-order.test.ts +27 -27
- package/src/__tests__/strictmode/react-strictmode-behavior.test.tsx +1 -1
- package/src/__tests__/strictmode/strictmode.test.ts +42 -42
- package/src/__tests__/strictmode/tap-strictmode-rerender-sources.test.ts +55 -55
- package/src/__tests__/test-utils.ts +2 -2
- package/src/core/ResourceFiber.ts +4 -1
- package/src/core/context.ts +31 -9
- package/src/core/createResourceRoot.ts +4 -4
- package/src/core/helpers/execution-context.ts +4 -0
- package/src/core/react-dispatcher.ts +78 -0
- package/src/core/scheduler.ts +1 -1
- package/src/core/types.ts +3 -3
- package/src/hooks/index.ts +12 -0
- package/src/hooks/use.ts +13 -0
- package/src/hooks/useCallback.ts +9 -0
- package/src/hooks/{tap-effect.ts → useEffect.ts} +9 -9
- package/src/hooks/{tap-effect-event.ts → useEffectEvent.ts} +9 -9
- package/src/hooks/{tap-memo.ts → useMemo.ts} +3 -3
- package/src/hooks/useMemoCache.ts +25 -0
- package/src/hooks/{tap-reducer.ts → useReducer.ts} +23 -11
- package/src/hooks/useRef.ts +16 -0
- package/src/hooks/{tap-resource.ts → useResource.ts} +13 -12
- package/src/{tapResourceRoot.ts → hooks/useResourceRoot.ts} +26 -27
- package/src/hooks/{tap-resources.ts → useResources.ts} +21 -22
- package/src/hooks/useState.ts +29 -0
- package/src/hooks/utils/{tapHook.ts → useCell.ts} +1 -1
- package/src/index.ts +4 -24
- package/src/react/hooks.ts +112 -0
- package/src/react-shim/index.ts +64 -0
- package/dist/hooks/tap-callback.d.ts +0 -5
- package/dist/hooks/tap-callback.d.ts.map +0 -1
- package/dist/hooks/tap-callback.js +0 -9
- package/dist/hooks/tap-callback.js.map +0 -1
- package/dist/hooks/tap-const.d.ts +0 -5
- package/dist/hooks/tap-const.d.ts.map +0 -1
- package/dist/hooks/tap-const.js +0 -10
- package/dist/hooks/tap-const.js.map +0 -1
- package/dist/hooks/tap-effect-event.d.ts.map +0 -1
- package/dist/hooks/tap-effect-event.js.map +0 -1
- package/dist/hooks/tap-effect.d.ts +0 -10
- package/dist/hooks/tap-effect.d.ts.map +0 -1
- package/dist/hooks/tap-effect.js.map +0 -1
- package/dist/hooks/tap-memo.d.ts +0 -5
- package/dist/hooks/tap-memo.d.ts.map +0 -1
- package/dist/hooks/tap-memo.js.map +0 -1
- package/dist/hooks/tap-reducer.d.ts +0 -9
- package/dist/hooks/tap-reducer.d.ts.map +0 -1
- package/dist/hooks/tap-reducer.js.map +0 -1
- package/dist/hooks/tap-ref.d.ts +0 -11
- package/dist/hooks/tap-ref.d.ts.map +0 -1
- package/dist/hooks/tap-ref.js +0 -10
- package/dist/hooks/tap-ref.js.map +0 -1
- package/dist/hooks/tap-resource.d.ts +0 -8
- package/dist/hooks/tap-resource.d.ts.map +0 -1
- package/dist/hooks/tap-resource.js.map +0 -1
- package/dist/hooks/tap-resources.d.ts.map +0 -1
- package/dist/hooks/tap-resources.js.map +0 -1
- package/dist/hooks/tap-state.d.ts +0 -9
- package/dist/hooks/tap-state.d.ts.map +0 -1
- package/dist/hooks/tap-state.js +0 -11
- package/dist/hooks/tap-state.js.map +0 -1
- package/dist/hooks/utils/tapHook.d.ts +0 -10
- package/dist/hooks/utils/tapHook.d.ts.map +0 -1
- package/dist/react/index.d.ts +0 -2
- package/dist/react/index.js +0 -2
- package/dist/react/use-resource.d.ts.map +0 -1
- package/dist/react/use-resource.js +0 -46
- package/dist/react/use-resource.js.map +0 -1
- package/dist/tapResourceRoot.d.ts +0 -20
- package/dist/tapResourceRoot.d.ts.map +0 -1
- package/dist/tapResourceRoot.js.map +0 -1
- package/react/package.json +0 -5
- package/src/hooks/tap-callback.ts +0 -9
- package/src/hooks/tap-const.ts +0 -6
- package/src/hooks/tap-ref.ts +0 -16
- package/src/hooks/tap-state.ts +0 -29
- package/src/react/index.ts +0 -1
- package/src/react/use-resource.ts +0 -61
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { useEffect } from "../../hooks/useEffect";
|
|
3
|
+
import { useState } from "../../hooks/useState";
|
|
4
4
|
import { createTestResource, renderTest, unmountResource } from "../test-utils";
|
|
5
5
|
import {
|
|
6
6
|
renderResourceFiber,
|
|
@@ -13,7 +13,7 @@ describe("Lifecycle - Mount/Unmount", () => {
|
|
|
13
13
|
const effects = [vi.fn(), vi.fn(), vi.fn()];
|
|
14
14
|
|
|
15
15
|
const resource = createTestResource(() => {
|
|
16
|
-
effects.forEach((fn) =>
|
|
16
|
+
effects.forEach((fn) => useEffect(fn));
|
|
17
17
|
return null;
|
|
18
18
|
});
|
|
19
19
|
|
|
@@ -29,7 +29,7 @@ describe("Lifecycle - Mount/Unmount", () => {
|
|
|
29
29
|
|
|
30
30
|
const resource = createTestResource(() => {
|
|
31
31
|
cleanups.forEach((cleanup) => {
|
|
32
|
-
|
|
32
|
+
useEffect(() => cleanup);
|
|
33
33
|
});
|
|
34
34
|
return null;
|
|
35
35
|
});
|
|
@@ -45,9 +45,9 @@ describe("Lifecycle - Mount/Unmount", () => {
|
|
|
45
45
|
const order: number[] = [];
|
|
46
46
|
|
|
47
47
|
const resource = createTestResource(() => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
useEffect(() => () => order.push(1));
|
|
49
|
+
useEffect(() => () => order.push(2));
|
|
50
|
+
useEffect(() => () => order.push(3));
|
|
51
51
|
return null;
|
|
52
52
|
});
|
|
53
53
|
|
|
@@ -64,11 +64,11 @@ describe("Lifecycle - Mount/Unmount", () => {
|
|
|
64
64
|
|
|
65
65
|
const resource = createTestResource((props: number) => {
|
|
66
66
|
renderCount++;
|
|
67
|
-
const [state, _setState] =
|
|
67
|
+
const [state, _setState] = useState({ count: 0 });
|
|
68
68
|
setState = _setState;
|
|
69
69
|
|
|
70
70
|
// Simple effect that tracks runs
|
|
71
|
-
|
|
71
|
+
useEffect(() => {
|
|
72
72
|
effectRunCount++;
|
|
73
73
|
});
|
|
74
74
|
|
|
@@ -100,18 +100,18 @@ describe("Lifecycle - Mount/Unmount", () => {
|
|
|
100
100
|
const log: string[] = [];
|
|
101
101
|
|
|
102
102
|
const resource = createTestResource(() => {
|
|
103
|
-
const [mounted, setMounted] =
|
|
103
|
+
const [mounted, setMounted] = useState(false);
|
|
104
104
|
|
|
105
105
|
log.push("render");
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
useEffect(() => {
|
|
108
108
|
log.push("effect-1");
|
|
109
109
|
setMounted(true);
|
|
110
110
|
|
|
111
111
|
return () => log.push("cleanup-1");
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
-
|
|
114
|
+
useEffect(() => {
|
|
115
115
|
log.push("effect-2");
|
|
116
116
|
return () => log.push("cleanup-2");
|
|
117
117
|
});
|
|
@@ -128,8 +128,7 @@ describe("Lifecycle - Mount/Unmount", () => {
|
|
|
128
128
|
// After commit: initial render + effects
|
|
129
129
|
expect(log).toEqual(["render", "effect-1", "effect-2"]);
|
|
130
130
|
|
|
131
|
-
// The setState in effect schedules a re-render
|
|
132
|
-
// With the new architecture, we need to manually trigger it
|
|
131
|
+
// The setState in effect schedules a re-render; trigger it manually
|
|
133
132
|
const ctx2 = renderResourceFiber(resource, undefined);
|
|
134
133
|
commitResourceFiber(resource, ctx2);
|
|
135
134
|
|
|
@@ -158,10 +157,10 @@ describe("Lifecycle - Mount/Unmount", () => {
|
|
|
158
157
|
const goodCleanup = vi.fn();
|
|
159
158
|
|
|
160
159
|
const resource = createTestResource(() => {
|
|
161
|
-
|
|
160
|
+
useEffect(() => () => {
|
|
162
161
|
throw error;
|
|
163
162
|
});
|
|
164
|
-
|
|
163
|
+
useEffect(() => goodCleanup);
|
|
165
164
|
return null;
|
|
166
165
|
});
|
|
167
166
|
|
|
@@ -178,7 +177,7 @@ describe("Lifecycle - Mount/Unmount", () => {
|
|
|
178
177
|
|
|
179
178
|
const resource = createTestResource(() => {
|
|
180
179
|
if (!skipEffect) {
|
|
181
|
-
|
|
180
|
+
useEffect(() => cleanup);
|
|
182
181
|
}
|
|
183
182
|
return null;
|
|
184
183
|
});
|
|
@@ -194,7 +193,7 @@ describe("Lifecycle - Mount/Unmount", () => {
|
|
|
194
193
|
const cleanup = vi.fn();
|
|
195
194
|
|
|
196
195
|
const resource = createTestResource(() => {
|
|
197
|
-
|
|
196
|
+
useEffect(() => {
|
|
198
197
|
effect();
|
|
199
198
|
return cleanup;
|
|
200
199
|
});
|
|
@@ -2,17 +2,17 @@ import { describe, it, expect } from "vitest";
|
|
|
2
2
|
import { render, screen, act } from "@testing-library/react";
|
|
3
3
|
import { Suspense, startTransition, use, useState } from "react";
|
|
4
4
|
import { resource } from "../../core/resource";
|
|
5
|
-
import { useResource } from "../../react/
|
|
6
|
-
import {
|
|
5
|
+
import { useResource } from "../../react/hooks";
|
|
6
|
+
import { useState as useResourceState } from "../../hooks/useState";
|
|
7
7
|
|
|
8
8
|
const ShouldNeverFallback = () => {
|
|
9
9
|
throw new Error("should never fallback");
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
describe("Concurrent Mode with useResource", () => {
|
|
13
|
-
it("should not commit
|
|
14
|
-
const TestResource = resource(()
|
|
15
|
-
return
|
|
13
|
+
it("should not commit useResourceState updates when render is discarded", async () => {
|
|
14
|
+
const TestResource = resource(function TestResource() {
|
|
15
|
+
return useResourceState(false);
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
let resolve: (value: number) => void;
|
|
@@ -76,7 +76,7 @@ describe("Concurrent Mode with useResource", () => {
|
|
|
76
76
|
expect(screen.getByTestId("message").textContent).toBe("hello");
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
-
it("react should not commit
|
|
79
|
+
it("react should not commit useResourceState updates when render is discarded", async () => {
|
|
80
80
|
let resolve: (value: number) => void;
|
|
81
81
|
|
|
82
82
|
const suspendPromise = new Promise<number>((r) => {
|
|
@@ -142,7 +142,7 @@ describe("Concurrent Mode with useResource", () => {
|
|
|
142
142
|
let resolve: () => void;
|
|
143
143
|
let shouldSuspend = false;
|
|
144
144
|
|
|
145
|
-
const TestResource = resource((props: { id: number })
|
|
145
|
+
const TestResource = resource(function TestResource(props: { id: number }) {
|
|
146
146
|
if (shouldSuspend) {
|
|
147
147
|
throw new Promise<void>((r) => {
|
|
148
148
|
resolve = r;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
+
import { render, screen, act, cleanup } from "@testing-library/react";
|
|
3
|
+
import {
|
|
4
|
+
createTestResource,
|
|
5
|
+
renderTest,
|
|
6
|
+
cleanupAllResources,
|
|
7
|
+
getCommittedOutput,
|
|
8
|
+
waitForNextTick,
|
|
9
|
+
} from "../test-utils";
|
|
10
|
+
import { useState, useEffect } from "../../react-shim";
|
|
11
|
+
|
|
12
|
+
describe("@assistant-ui/tap/react-shim", () => {
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
cleanupAllResources();
|
|
15
|
+
cleanup();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("inside a tap resource", () => {
|
|
19
|
+
it("useState routes to useState and useEffect to useEffect", async () => {
|
|
20
|
+
let setCount: ((n: number) => void) | null = null;
|
|
21
|
+
const effectLog: number[] = [];
|
|
22
|
+
|
|
23
|
+
const testFiber = createTestResource(() => {
|
|
24
|
+
const [count, set] = useState(0);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
setCount = set;
|
|
27
|
+
effectLog.push(count);
|
|
28
|
+
}, [count]);
|
|
29
|
+
return count;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
renderTest(testFiber, undefined);
|
|
33
|
+
expect(getCommittedOutput(testFiber)).toBe(0);
|
|
34
|
+
expect(effectLog).toEqual([0]);
|
|
35
|
+
|
|
36
|
+
setCount!(5);
|
|
37
|
+
await waitForNextTick();
|
|
38
|
+
expect(getCommittedOutput(testFiber)).toBe(5);
|
|
39
|
+
expect(effectLog).toEqual([0, 5]);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("inside a React component", () => {
|
|
44
|
+
it("useState routes to React.useState", () => {
|
|
45
|
+
function Counter() {
|
|
46
|
+
const [count, setCount] = useState(0);
|
|
47
|
+
return (
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
data-testid="btn"
|
|
51
|
+
onClick={() => setCount(count + 1)}
|
|
52
|
+
>
|
|
53
|
+
{count}
|
|
54
|
+
</button>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
render(<Counter />);
|
|
59
|
+
const btn = screen.getByTestId("btn");
|
|
60
|
+
expect(btn.textContent).toBe("0");
|
|
61
|
+
act(() => btn.click());
|
|
62
|
+
expect(btn.textContent).toBe("1");
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
+
import { render, screen, act, cleanup } from "@testing-library/react";
|
|
3
|
+
import {
|
|
4
|
+
createTestResource,
|
|
5
|
+
renderTest,
|
|
6
|
+
cleanupAllResources,
|
|
7
|
+
} from "../test-utils";
|
|
8
|
+
import { resource } from "../../core/resource";
|
|
9
|
+
import { withKey } from "../../core/withKey";
|
|
10
|
+
import { useState } from "react";
|
|
11
|
+
import { useState as useResourceState } from "../../hooks/useState";
|
|
12
|
+
import { useEffect as useResourceEffect } from "../../hooks/useEffect";
|
|
13
|
+
import {
|
|
14
|
+
useResource,
|
|
15
|
+
useResources,
|
|
16
|
+
useResourceRoot,
|
|
17
|
+
flushResourcesSync,
|
|
18
|
+
} from "../../index";
|
|
19
|
+
|
|
20
|
+
describe("@assistant-ui/tap/react resource API", () => {
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
cleanupAllResources();
|
|
23
|
+
cleanup();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("useResource", () => {
|
|
27
|
+
it("routes to useResource inside a tap resource", () => {
|
|
28
|
+
const Child = resource(function Child(props: { n: number }) {
|
|
29
|
+
return props.n * 2;
|
|
30
|
+
});
|
|
31
|
+
const parent = createTestResource(() => useResource(Child({ n: 21 })));
|
|
32
|
+
expect(renderTest(parent, undefined)).toBe(42);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("routes to the React bridge inside a component", () => {
|
|
36
|
+
const CounterResource = resource(function CounterResource() {
|
|
37
|
+
const [count, setCount] = useResourceState(0);
|
|
38
|
+
return { count, setCount };
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
let api: { count: number; setCount: (n: number) => void } | null = null;
|
|
42
|
+
function App() {
|
|
43
|
+
api = useResource(CounterResource());
|
|
44
|
+
return <div data-testid="count">{api.count}</div>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
render(<App />);
|
|
48
|
+
expect(screen.getByTestId("count").textContent).toBe("0");
|
|
49
|
+
act(() => api!.setCount(3));
|
|
50
|
+
expect(screen.getByTestId("count").textContent).toBe("3");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("useResources", () => {
|
|
55
|
+
it("hosts a keyed list inside a tap resource", () => {
|
|
56
|
+
const Item = resource(function Item(p: { n: number }) {
|
|
57
|
+
return p.n * 10;
|
|
58
|
+
});
|
|
59
|
+
const parent = createTestResource(() =>
|
|
60
|
+
useResources(() => [
|
|
61
|
+
withKey("a", Item({ n: 1 })),
|
|
62
|
+
withKey("b", Item({ n: 2 })),
|
|
63
|
+
]),
|
|
64
|
+
);
|
|
65
|
+
expect(renderTest(parent, undefined)).toEqual([10, 20]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("hosts a keyed list inside a React component and tracks deps", () => {
|
|
69
|
+
const Item = resource(function Item(p: { n: number }) {
|
|
70
|
+
const [v] = useResourceState(p.n * 10);
|
|
71
|
+
return v;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
let setCount: (n: number) => void = () => {};
|
|
75
|
+
function App() {
|
|
76
|
+
const [count, setCountState] = useState(2);
|
|
77
|
+
setCount = setCountState;
|
|
78
|
+
const items = useResources(
|
|
79
|
+
() =>
|
|
80
|
+
Array.from({ length: count }, (_, i) =>
|
|
81
|
+
withKey(i, Item({ n: i + 1 })),
|
|
82
|
+
),
|
|
83
|
+
[count],
|
|
84
|
+
);
|
|
85
|
+
return <div data-testid="list">{items.join(",")}</div>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
render(<App />);
|
|
89
|
+
expect(screen.getByTestId("list").textContent).toBe("10,20");
|
|
90
|
+
act(() => setCount(3));
|
|
91
|
+
expect(screen.getByTestId("list").textContent).toBe("10,20,30");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("useResourceRoot", () => {
|
|
96
|
+
it("exposes a subscribable inside a tap resource", () => {
|
|
97
|
+
const Root = resource(function Root() {
|
|
98
|
+
const [n] = useResourceState(7);
|
|
99
|
+
return n;
|
|
100
|
+
});
|
|
101
|
+
const parent = createTestResource(() =>
|
|
102
|
+
useResourceRoot(Root()).getValue(),
|
|
103
|
+
);
|
|
104
|
+
expect(renderTest(parent, undefined)).toBe(7);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// A root is push-based: host it in one place and observe it via getValue/
|
|
108
|
+
// subscribe elsewhere. (Hosting AND re-rendering off its own value in the same
|
|
109
|
+
// component self-feeds, since useResourceHost re-renders the root on every host render
|
|
110
|
+
// and the root notifies on output change — so this test observes the store
|
|
111
|
+
// directly rather than through a same-component useSyncExternalStore.)
|
|
112
|
+
it("hosts a subscribable root inside a React component", () => {
|
|
113
|
+
const CounterRoot = resource(function CounterRoot() {
|
|
114
|
+
const [count, setCount] = useResourceState(0);
|
|
115
|
+
return { count, setCount };
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
let store: ReturnType<
|
|
119
|
+
typeof useResourceRoot<{
|
|
120
|
+
count: number;
|
|
121
|
+
setCount: (n: number) => void;
|
|
122
|
+
}>
|
|
123
|
+
> | null = null;
|
|
124
|
+
function App() {
|
|
125
|
+
store = useResourceRoot(CounterRoot());
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
render(<App />);
|
|
130
|
+
expect(store!.getValue().count).toBe(0);
|
|
131
|
+
|
|
132
|
+
let notified = 0;
|
|
133
|
+
const unsubscribe = store!.subscribe(() => {
|
|
134
|
+
notified++;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// The root drives updates through tap's own (macrotask) scheduler, so flush
|
|
138
|
+
// synchronously to observe.
|
|
139
|
+
flushResourcesSync(() => store!.getValue().setCount(5));
|
|
140
|
+
expect(store!.getValue().count).toBe(5);
|
|
141
|
+
expect(notified).toBeGreaterThan(0);
|
|
142
|
+
|
|
143
|
+
unsubscribe();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("useResource key remount (React bridge)", () => {
|
|
148
|
+
it("remounts the hosted resource when the element key changes", () => {
|
|
149
|
+
const mounts: number[] = [];
|
|
150
|
+
const Keyed = resource(function Keyed(p: { id: number }) {
|
|
151
|
+
// oxlint-disable-next-line react/exhaustive-deps -- capture the mount id once per fiber to assert remount on key change
|
|
152
|
+
useResourceEffect(() => void mounts.push(p.id), []);
|
|
153
|
+
return p.id;
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
let setId: (n: number) => void = () => {};
|
|
157
|
+
function App() {
|
|
158
|
+
const [id, setIdState] = useState(1);
|
|
159
|
+
setId = setIdState;
|
|
160
|
+
const out = useResource(withKey(id, Keyed({ id })));
|
|
161
|
+
return <div data-testid="keyed">{out}</div>;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
render(<App />);
|
|
165
|
+
expect(screen.getByTestId("keyed").textContent).toBe("1");
|
|
166
|
+
expect(mounts).toEqual([1]);
|
|
167
|
+
act(() => setId(2));
|
|
168
|
+
expect(screen.getByTestId("keyed").textContent).toBe("2");
|
|
169
|
+
expect(mounts).toEqual([1, 2]);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
// react/compiler-runtime exports `c` (= useMemoCache) at runtime but ships no types for it.
|
|
4
|
+
// @ts-expect-error -- runtime-only export
|
|
5
|
+
import { c as _c } from "react/compiler-runtime";
|
|
6
|
+
import { renderResourceFiber } from "../core/ResourceFiber";
|
|
7
|
+
import {
|
|
8
|
+
createTestResource,
|
|
9
|
+
renderTest,
|
|
10
|
+
getCommittedOutput,
|
|
11
|
+
cleanupAllResources,
|
|
12
|
+
waitForNextTick,
|
|
13
|
+
} from "./test-utils";
|
|
14
|
+
|
|
15
|
+
// These resources author their hooks with the *real* `react` module (no shim, no
|
|
16
|
+
// build transform). tap's React dispatcher, installed around every resource body
|
|
17
|
+
// render, is what routes them to tap.
|
|
18
|
+
describe("react dispatcher", () => {
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
cleanupAllResources();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("routes React.useState to tap state", async () => {
|
|
24
|
+
let set!: (n: number) => void;
|
|
25
|
+
const fiber = createTestResource(() => {
|
|
26
|
+
const [n, setN] = React.useState(10);
|
|
27
|
+
set = setN;
|
|
28
|
+
return n;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(renderTest(fiber, undefined)).toBe(10);
|
|
32
|
+
set(42);
|
|
33
|
+
await waitForNextTick();
|
|
34
|
+
expect(getCommittedOutput(fiber)).toBe(42);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("routes React.useMemo with deps memoization", () => {
|
|
38
|
+
let runs = 0;
|
|
39
|
+
const fiber = createTestResource((p: { x: number }) =>
|
|
40
|
+
React.useMemo(() => {
|
|
41
|
+
runs++;
|
|
42
|
+
return p.x * 2;
|
|
43
|
+
}, [p.x]),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
expect(renderTest(fiber, { x: 2 })).toBe(4);
|
|
47
|
+
expect(runs).toBe(1);
|
|
48
|
+
renderTest(fiber, { x: 2 }); // same dep -> memoized
|
|
49
|
+
expect(runs).toBe(1);
|
|
50
|
+
expect(renderTest(fiber, { x: 3 })).toBe(6);
|
|
51
|
+
expect(runs).toBe(2);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("backs react/compiler-runtime's useMemoCache so compiled resources work", () => {
|
|
55
|
+
const SENTINEL = Symbol.for("react.memo_cache_sentinel");
|
|
56
|
+
const fiber = createTestResource(() => {
|
|
57
|
+
// exactly what React Compiler emits: const $ = _c(n)
|
|
58
|
+
const $ = _c(3);
|
|
59
|
+
if ($[0] === SENTINEL) $[0] = "computed-once";
|
|
60
|
+
return $[0];
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(renderTest(fiber, undefined)).toBe("computed-once");
|
|
64
|
+
// re-render: the cache persists across renders, slot already filled
|
|
65
|
+
expect(renderTest(fiber, undefined)).toBe("computed-once");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("throws for a hook tap does not implement", () => {
|
|
69
|
+
const fiber = createTestResource(() => React.useId());
|
|
70
|
+
// render directly: a mid-render throw must not leave a tracked, unmounted
|
|
71
|
+
// fiber for `cleanupAllResources` to choke on.
|
|
72
|
+
expect(() => renderResourceFiber(fiber, undefined)).toThrow();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -1,17 +1,18 @@
|
|
|
1
|
+
/* oxlint-disable react/rules-of-hooks -- tests deliberately exercise conditional/nested hook patterns */
|
|
1
2
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
3
|
+
import { useEffect } from "../../hooks/useEffect";
|
|
4
|
+
import { useState } from "../../hooks/useState";
|
|
4
5
|
import { createTestResource, renderTest } from "../test-utils";
|
|
5
6
|
import { renderResourceFiber } from "../../core/ResourceFiber";
|
|
6
7
|
|
|
7
8
|
describe("Rules of Hooks - Hook Count", () => {
|
|
8
9
|
it("should establish hook count on first render", () => {
|
|
9
10
|
const resource = createTestResource(() => {
|
|
10
|
-
const [a] =
|
|
11
|
-
const [b] =
|
|
12
|
-
const [c] =
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
const [a] = useState(1);
|
|
12
|
+
const [b] = useState(2);
|
|
13
|
+
const [c] = useState(3);
|
|
14
|
+
useEffect(() => {});
|
|
15
|
+
useEffect(() => {});
|
|
15
16
|
|
|
16
17
|
return { a, b, c };
|
|
17
18
|
});
|
|
@@ -29,11 +30,11 @@ describe("Rules of Hooks - Hook Count", () => {
|
|
|
29
30
|
let addExtraHook = false;
|
|
30
31
|
|
|
31
32
|
const resource = createTestResource(() => {
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
useState(1);
|
|
34
|
+
useState(2);
|
|
34
35
|
|
|
35
36
|
if (addExtraHook) {
|
|
36
|
-
|
|
37
|
+
useState(3); // Extra hook
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
return null;
|
|
@@ -54,13 +55,13 @@ describe("Rules of Hooks - Hook Count", () => {
|
|
|
54
55
|
let skipHook = false;
|
|
55
56
|
|
|
56
57
|
const resource = createTestResource(() => {
|
|
57
|
-
|
|
58
|
+
useState(1);
|
|
58
59
|
|
|
59
60
|
if (!skipHook) {
|
|
60
|
-
|
|
61
|
+
useState(2);
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
useState(3);
|
|
64
65
|
return null;
|
|
65
66
|
});
|
|
66
67
|
|
|
@@ -79,11 +80,11 @@ describe("Rules of Hooks - Hook Count", () => {
|
|
|
79
80
|
let includeEffect = true;
|
|
80
81
|
|
|
81
82
|
const resource = createTestResource(() => {
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
useState(1);
|
|
84
|
+
useState(2);
|
|
84
85
|
|
|
85
86
|
if (includeEffect) {
|
|
86
|
-
|
|
87
|
+
useEffect(() => {});
|
|
87
88
|
}
|
|
88
89
|
return null;
|
|
89
90
|
});
|
|
@@ -114,7 +115,7 @@ describe("Rules of Hooks - Hook Count", () => {
|
|
|
114
115
|
|
|
115
116
|
const resource = createTestResource(() => {
|
|
116
117
|
for (let i = 0; i < hookCount; i++) {
|
|
117
|
-
|
|
118
|
+
useState(i);
|
|
118
119
|
}
|
|
119
120
|
return null;
|
|
120
121
|
});
|
|
@@ -134,9 +135,9 @@ describe("Rules of Hooks - Hook Count", () => {
|
|
|
134
135
|
|
|
135
136
|
const resource = createTestResource(() => {
|
|
136
137
|
renderCount++;
|
|
137
|
-
const [a] =
|
|
138
|
-
const [b] =
|
|
139
|
-
|
|
138
|
+
const [a] = useState(1);
|
|
139
|
+
const [b] = useState(2);
|
|
140
|
+
useEffect(() => {});
|
|
140
141
|
|
|
141
142
|
return { a, b, renderCount };
|
|
142
143
|
});
|
|
@@ -151,16 +152,16 @@ describe("Rules of Hooks - Hook Count", () => {
|
|
|
151
152
|
|
|
152
153
|
it("should track count separately for different resource instances", () => {
|
|
153
154
|
const resource1 = createTestResource(() => {
|
|
154
|
-
|
|
155
|
-
|
|
155
|
+
useState(1);
|
|
156
|
+
useState(2);
|
|
156
157
|
return "two hooks";
|
|
157
158
|
});
|
|
158
159
|
|
|
159
160
|
const resource2 = createTestResource(() => {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
161
|
+
useState(1);
|
|
162
|
+
useState(2);
|
|
163
|
+
useState(3);
|
|
164
|
+
useEffect(() => {});
|
|
164
165
|
return "four hooks";
|
|
165
166
|
});
|
|
166
167
|
|
|
@@ -177,14 +178,14 @@ describe("Rules of Hooks - Hook Count", () => {
|
|
|
177
178
|
let useExtraHooks = false;
|
|
178
179
|
|
|
179
180
|
const useFeature = () => {
|
|
180
|
-
|
|
181
|
+
useState("feature");
|
|
181
182
|
if (useExtraHooks) {
|
|
182
|
-
|
|
183
|
+
useState("extra");
|
|
183
184
|
}
|
|
184
185
|
};
|
|
185
186
|
|
|
186
187
|
const resource = createTestResource(() => {
|
|
187
|
-
|
|
188
|
+
useState("main");
|
|
188
189
|
useFeature();
|
|
189
190
|
return null;
|
|
190
191
|
});
|