@assistant-ui/tap 0.5.13 → 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/commit.d.ts.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/helpers/root.d.ts.map +1 -1
- package/dist/core/helpers/root.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/resource.d.ts.map +1 -1
- package/dist/core/scheduler.d.ts.map +1 -1
- 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/core/withKey.d.ts.map +1 -1
- 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 -21
- 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 -21
- 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 -4
- package/src/__tests__/strictmode/strictmode.test.ts +42 -42
- package/src/__tests__/strictmode/tap-strictmode-rerender-sources.test.ts +55 -58
- package/src/__tests__/test-utils.ts +2 -3
- 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/helpers/root.ts +0 -1
- 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 -29
- 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
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
commitResourceFiber,
|
|
8
8
|
} from "../core/ResourceFiber";
|
|
9
9
|
import type { ResourceFiber } from "../core/types";
|
|
10
|
-
import {
|
|
10
|
+
import { useState } from "../hooks/useState";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Creates a test resource fiber for unit testing.
|
|
@@ -74,7 +74,6 @@ export function unmountResource<R, P>(fiber: ResourceFiber<R, P>) {
|
|
|
74
74
|
* Cleans up all resources. Should be called after each test.
|
|
75
75
|
*/
|
|
76
76
|
export function cleanupAllResources() {
|
|
77
|
-
// biome-ignore lint/suspicious/useIterableCallbackReturn: forEach callback intentionally has no return
|
|
78
77
|
activeResources.forEach((fiber) => unmountResourceFiber(fiber));
|
|
79
78
|
activeResources.clear();
|
|
80
79
|
}
|
|
@@ -196,7 +195,7 @@ export function createCounterResource(initialValue = 0) {
|
|
|
196
195
|
*/
|
|
197
196
|
export function createStatefulCounterResource() {
|
|
198
197
|
return (props: { initial: number }) => {
|
|
199
|
-
const [count, setCount] =
|
|
198
|
+
const [count, setCount] = useState(props.initial);
|
|
200
199
|
return {
|
|
201
200
|
count,
|
|
202
201
|
increment: () => setCount((c: number) => c + 1),
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
withResourceFiber,
|
|
11
11
|
} from "./helpers/execution-context";
|
|
12
12
|
import { callResourceFn } from "./helpers/callResourceFn";
|
|
13
|
+
import { withReactDispatcher } from "./react-dispatcher";
|
|
13
14
|
import { isDevelopment } from "./helpers/env";
|
|
14
15
|
|
|
15
16
|
export function createResourceFiber<R, P>(
|
|
@@ -53,7 +54,9 @@ export function renderResourceFiber<R, P>(
|
|
|
53
54
|
withResourceFiber(fiber, () => {
|
|
54
55
|
fiber.renderContext = result;
|
|
55
56
|
try {
|
|
56
|
-
result.output =
|
|
57
|
+
result.output = withReactDispatcher(() =>
|
|
58
|
+
callResourceFn(fiber.type, props),
|
|
59
|
+
);
|
|
57
60
|
} finally {
|
|
58
61
|
fiber.renderContext = undefined;
|
|
59
62
|
}
|
package/src/core/context.ts
CHANGED
|
@@ -1,28 +1,50 @@
|
|
|
1
|
+
import type { Context as ReactContext } from "react";
|
|
2
|
+
|
|
1
3
|
const contextValue: unique symbol = Symbol("tap.Context");
|
|
2
|
-
type
|
|
4
|
+
type TapContext<T> = {
|
|
3
5
|
[contextValue]: T;
|
|
4
6
|
};
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
const asTap = <T>(context: ReactContext<T>): TapContext<T> =>
|
|
9
|
+
context as unknown as TapContext<T>;
|
|
10
|
+
|
|
11
|
+
// A tap resource context is typed as a React `Context<T>` purely so React's
|
|
12
|
+
// `use(context)` accepts it (the type is erased, so this adds no runtime react
|
|
13
|
+
// dependency). At runtime it is only a branded `{ [contextValue]: T }` and is
|
|
14
|
+
// not a usable React context — `use()` routes it to `useResourceContext()` via the brand.
|
|
15
|
+
/**
|
|
16
|
+
* @deprecated experimental — the resource context API is not yet stable and may
|
|
17
|
+
* change or be removed in a future release.
|
|
18
|
+
*/
|
|
19
|
+
export const createResourceContext = <T>(defaultValue: T): ReactContext<T> => {
|
|
7
20
|
return {
|
|
8
21
|
[contextValue]: defaultValue,
|
|
9
|
-
}
|
|
22
|
+
} as unknown as ReactContext<T>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const isResourceContext = (value: unknown): boolean => {
|
|
26
|
+
return typeof value === "object" && value !== null && contextValue in value;
|
|
10
27
|
};
|
|
11
28
|
|
|
29
|
+
/**
|
|
30
|
+
* @deprecated experimental — the resource context API is not yet stable and may
|
|
31
|
+
* change or be removed in a future release.
|
|
32
|
+
*/
|
|
12
33
|
export const withContextProvider = <T, TResult>(
|
|
13
|
-
context:
|
|
34
|
+
context: ReactContext<T>,
|
|
14
35
|
value: T,
|
|
15
36
|
fn: () => TResult,
|
|
16
37
|
) => {
|
|
17
|
-
const
|
|
18
|
-
|
|
38
|
+
const tapContext = asTap(context);
|
|
39
|
+
const previousValue = tapContext[contextValue];
|
|
40
|
+
tapContext[contextValue] = value;
|
|
19
41
|
try {
|
|
20
42
|
return fn();
|
|
21
43
|
} finally {
|
|
22
|
-
|
|
44
|
+
tapContext[contextValue] = previousValue;
|
|
23
45
|
}
|
|
24
46
|
};
|
|
25
47
|
|
|
26
|
-
export const
|
|
27
|
-
return context[contextValue];
|
|
48
|
+
export const useResourceContext = <T>(context: ReactContext<T>) => {
|
|
49
|
+
return asTap(context)[contextValue];
|
|
28
50
|
};
|
|
@@ -5,17 +5,17 @@ import {
|
|
|
5
5
|
renderResourceFiber,
|
|
6
6
|
commitResourceFiber,
|
|
7
7
|
} from "./ResourceFiber";
|
|
8
|
-
import {
|
|
8
|
+
import { useResourceRoot } from "../hooks/useResourceRoot";
|
|
9
9
|
import { resource } from "./resource";
|
|
10
10
|
import { isDevelopment } from "./helpers/env";
|
|
11
11
|
import { flushResourcesSync, UpdateScheduler } from "./scheduler";
|
|
12
12
|
import { createResourceFiberRoot } from "./helpers/root";
|
|
13
13
|
|
|
14
|
-
const SubscribableResource = resource(
|
|
14
|
+
const SubscribableResource = resource(useResourceRoot);
|
|
15
15
|
|
|
16
16
|
export const createResourceRoot = () => {
|
|
17
17
|
const fiber = createResourceFiber<
|
|
18
|
-
|
|
18
|
+
useResourceRoot.SubscribableResource<any>,
|
|
19
19
|
ResourceElement<any>
|
|
20
20
|
>(
|
|
21
21
|
SubscribableResource,
|
|
@@ -44,7 +44,7 @@ export const createResourceRoot = () => {
|
|
|
44
44
|
|
|
45
45
|
flushResourcesSync(() => commitResourceFiber(fiber, render));
|
|
46
46
|
|
|
47
|
-
return render.output
|
|
47
|
+
return render.output as useResourceRoot.SubscribableResource<R>;
|
|
48
48
|
},
|
|
49
49
|
unmount: () => {
|
|
50
50
|
unmountResourceFiber(fiber);
|
|
@@ -34,6 +34,10 @@ export function getCurrentResourceFiber(): ResourceFiber<unknown, unknown> {
|
|
|
34
34
|
return currentResourceFiber;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
export function peekResourceFiber(): ResourceFiber<unknown, unknown> | null {
|
|
38
|
+
return currentResourceFiber;
|
|
39
|
+
}
|
|
40
|
+
|
|
37
41
|
export function getDevStrictMode(enable: boolean) {
|
|
38
42
|
if (!isDevelopment) return null;
|
|
39
43
|
if (currentResourceFiber?.devStrictMode)
|
package/src/core/helpers/root.ts
CHANGED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { useState } from "../hooks/useState";
|
|
3
|
+
import { useReducer } from "../hooks/useReducer";
|
|
4
|
+
import { useRef } from "../hooks/useRef";
|
|
5
|
+
import { useMemo } from "../hooks/useMemo";
|
|
6
|
+
import { useCallback } from "../hooks/useCallback";
|
|
7
|
+
import { useEffect } from "../hooks/useEffect";
|
|
8
|
+
import { useEffectEvent } from "../hooks/useEffectEvent";
|
|
9
|
+
import { use } from "../hooks/use";
|
|
10
|
+
import { useMemoCache } from "../hooks/useMemoCache";
|
|
11
|
+
|
|
12
|
+
// The dispatcher React reads while a resource renders, so hooks imported from
|
|
13
|
+
// "react" route to tap with no build step. Hooks tap has no equivalent for are
|
|
14
|
+
// intentionally absent: calling one throws, which is the intended "unsupported
|
|
15
|
+
// in a resource" signal.
|
|
16
|
+
const tapDispatcher = {
|
|
17
|
+
useState,
|
|
18
|
+
useReducer,
|
|
19
|
+
useRef,
|
|
20
|
+
useMemo,
|
|
21
|
+
useCallback,
|
|
22
|
+
useEffect,
|
|
23
|
+
useLayoutEffect: useEffect,
|
|
24
|
+
useInsertionEffect: useEffect,
|
|
25
|
+
useEffectEvent,
|
|
26
|
+
useContext: use,
|
|
27
|
+
use,
|
|
28
|
+
useMemoCache,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// React's live dispatcher slot differs by version: React 19 exposes it as `H` on
|
|
32
|
+
// the client internals object; React 18 as `ReactCurrentDispatcher.current`.
|
|
33
|
+
const internals =
|
|
34
|
+
(React as any)
|
|
35
|
+
.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE ??
|
|
36
|
+
(React as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
|
|
37
|
+
|
|
38
|
+
const slot: { current: unknown } | null =
|
|
39
|
+
internals == null
|
|
40
|
+
? null
|
|
41
|
+
: "H" in internals
|
|
42
|
+
? {
|
|
43
|
+
get current() {
|
|
44
|
+
return internals.H;
|
|
45
|
+
},
|
|
46
|
+
set current(d) {
|
|
47
|
+
internals.H = d;
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
: "ReactCurrentDispatcher" in internals
|
|
51
|
+
? {
|
|
52
|
+
get current() {
|
|
53
|
+
return internals.ReactCurrentDispatcher.current;
|
|
54
|
+
},
|
|
55
|
+
set current(d) {
|
|
56
|
+
internals.ReactCurrentDispatcher.current = d;
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
: null;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Runs a resource body with tap's React dispatcher installed, so real React
|
|
63
|
+
* hooks called inside it (`import { useState } from "react"`) route to tap, then
|
|
64
|
+
* restores the previous dispatcher. If React's internal dispatcher slot can't be
|
|
65
|
+
* found (an unsupported React version), the body runs unchanged and `react`
|
|
66
|
+
* hooks inside it keep throwing React's "invalid hook call".
|
|
67
|
+
*/
|
|
68
|
+
export function withReactDispatcher<T>(render: () => T): T {
|
|
69
|
+
if (!slot) return render();
|
|
70
|
+
|
|
71
|
+
const previous = slot.current;
|
|
72
|
+
slot.current = tapDispatcher;
|
|
73
|
+
try {
|
|
74
|
+
return render();
|
|
75
|
+
} finally {
|
|
76
|
+
slot.current = previous;
|
|
77
|
+
}
|
|
78
|
+
}
|
package/src/core/scheduler.ts
CHANGED
|
@@ -53,7 +53,7 @@ const flushScheduled = () => {
|
|
|
53
53
|
if (flushDepth > MAX_FLUSH_LIMIT) {
|
|
54
54
|
throw new Error(
|
|
55
55
|
`Maximum update depth exceeded. This can happen when a resource ` +
|
|
56
|
-
`repeatedly calls setState inside
|
|
56
|
+
`repeatedly calls setState inside useEffect.`,
|
|
57
57
|
);
|
|
58
58
|
}
|
|
59
59
|
|
package/src/core/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { useEffect } from "../hooks/useEffect";
|
|
2
2
|
|
|
3
3
|
export type ResourceElement<R, P = any> = {
|
|
4
4
|
readonly type: Resource<R, P>;
|
|
@@ -35,12 +35,12 @@ export type Cell =
|
|
|
35
35
|
}
|
|
36
36
|
| {
|
|
37
37
|
readonly type: "effect";
|
|
38
|
-
cleanup:
|
|
38
|
+
cleanup: useEffect.Destructor | undefined;
|
|
39
39
|
deps: readonly unknown[] | null | undefined;
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
export interface EffectTask {
|
|
43
|
-
readonly effect:
|
|
43
|
+
readonly effect: useEffect.EffectCallback;
|
|
44
44
|
readonly deps: readonly unknown[] | undefined;
|
|
45
45
|
readonly cell: Cell & { type: "effect" };
|
|
46
46
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { useState } from "./useState";
|
|
2
|
+
export { useReducer, useReducerWithDerivedState } from "./useReducer";
|
|
3
|
+
export { useRef } from "./useRef";
|
|
4
|
+
export { useMemo } from "./useMemo";
|
|
5
|
+
export { useCallback } from "./useCallback";
|
|
6
|
+
export { useEffect } from "./useEffect";
|
|
7
|
+
export { useEffectEvent } from "./useEffectEvent";
|
|
8
|
+
export { use } from "./use";
|
|
9
|
+
export { useMemoCache } from "./useMemoCache";
|
|
10
|
+
export { useResource } from "./useResource";
|
|
11
|
+
export { useResources } from "./useResources";
|
|
12
|
+
export { useResourceRoot } from "./useResourceRoot";
|
package/src/hooks/use.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { isResourceContext, useResourceContext } from "../core/context";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reads a resource context from inside a resource render, the tap equivalent of
|
|
5
|
+
* React's `use(Context)`. Only resource contexts are supported.
|
|
6
|
+
*/
|
|
7
|
+
export const use = (usable: unknown): unknown => {
|
|
8
|
+
if (!isResourceContext(usable))
|
|
9
|
+
throw new Error(
|
|
10
|
+
"A tap resource's `use()` only accepts a resource context.",
|
|
11
|
+
);
|
|
12
|
+
return useResourceContext(usable as never);
|
|
13
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { useMemo } from "./useMemo";
|
|
2
|
+
|
|
3
|
+
export const useCallback = <T extends (...args: any[]) => any>(
|
|
4
|
+
fn: T,
|
|
5
|
+
deps: readonly unknown[],
|
|
6
|
+
): T => {
|
|
7
|
+
// oxlint-disable-next-line react/exhaustive-deps -- user-provided dep array forwarded verbatim
|
|
8
|
+
return useMemo(() => fn, deps);
|
|
9
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Cell } from "../core/types";
|
|
2
2
|
import { depsShallowEqual } from "./utils/depsShallowEqual";
|
|
3
|
-
import {
|
|
3
|
+
import { useCell, registerRenderMountTask } from "./utils/useCell";
|
|
4
4
|
|
|
5
5
|
const newEffect = (): Cell & { type: "effect" } => ({
|
|
6
6
|
type: "effect",
|
|
@@ -8,26 +8,26 @@ const newEffect = (): Cell & { type: "effect" } => ({
|
|
|
8
8
|
deps: null, // null means the effect has never been run
|
|
9
9
|
});
|
|
10
10
|
|
|
11
|
-
export namespace
|
|
11
|
+
export namespace useEffect {
|
|
12
12
|
export type Destructor = () => void;
|
|
13
13
|
export type EffectCallback = () => Destructor | undefined;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export function
|
|
17
|
-
export function
|
|
18
|
-
effect:
|
|
16
|
+
export function useEffect(effect: useEffect.EffectCallback): void;
|
|
17
|
+
export function useEffect(
|
|
18
|
+
effect: useEffect.EffectCallback,
|
|
19
19
|
deps: readonly unknown[],
|
|
20
20
|
): void;
|
|
21
|
-
export function
|
|
22
|
-
effect:
|
|
21
|
+
export function useEffect(
|
|
22
|
+
effect: useEffect.EffectCallback,
|
|
23
23
|
deps?: readonly unknown[],
|
|
24
24
|
): void {
|
|
25
|
-
const cell =
|
|
25
|
+
const cell = useCell("effect", newEffect);
|
|
26
26
|
|
|
27
27
|
if (deps && cell.deps && depsShallowEqual(cell.deps, deps)) return;
|
|
28
28
|
if (cell.deps !== null && !!deps !== !!cell.deps)
|
|
29
29
|
throw new Error(
|
|
30
|
-
"
|
|
30
|
+
"useEffect called with and without dependencies across re-renders",
|
|
31
31
|
);
|
|
32
32
|
|
|
33
33
|
registerRenderMountTask(() => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { useRef } from "./useRef";
|
|
2
|
+
import { useEffect } from "./useEffect";
|
|
3
3
|
import { isDevelopment } from "../core/helpers/env";
|
|
4
|
-
import {
|
|
4
|
+
import { useCallback } from "./useCallback";
|
|
5
5
|
import { getCurrentResourceFiber } from "../core/helpers/execution-context";
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -13,27 +13,27 @@ import { getCurrentResourceFiber } from "../core/helpers/execution-context";
|
|
|
13
13
|
*
|
|
14
14
|
* @example
|
|
15
15
|
* ```typescript
|
|
16
|
-
* const handleClick =
|
|
16
|
+
* const handleClick = useEffectEvent((value: string) => {
|
|
17
17
|
* console.log(value);
|
|
18
18
|
* });
|
|
19
19
|
* // handleClick reference is stable, but always calls the latest version
|
|
20
20
|
* ```
|
|
21
21
|
*/
|
|
22
|
-
export function
|
|
22
|
+
export function useEffectEvent<T extends (...args: any[]) => any>(
|
|
23
23
|
callback: T,
|
|
24
24
|
): T {
|
|
25
|
-
const callbackRef =
|
|
25
|
+
const callbackRef = useRef(callback);
|
|
26
26
|
|
|
27
27
|
// TODO this effect needs to run before all userland effects
|
|
28
|
-
|
|
28
|
+
useEffect(() => {
|
|
29
29
|
callbackRef.current = callback;
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
const fiber = getCurrentResourceFiber();
|
|
33
|
-
return
|
|
33
|
+
return useCallback(
|
|
34
34
|
((...args: Parameters<T>) => {
|
|
35
35
|
if (isDevelopment && fiber.renderContext)
|
|
36
|
-
throw new Error("
|
|
36
|
+
throw new Error("useEffectEvent cannot be called during render");
|
|
37
37
|
return callbackRef.current(...args);
|
|
38
38
|
}) as T,
|
|
39
39
|
[fiber],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isDevelopment } from "../core/helpers/env";
|
|
2
2
|
import { getCurrentResourceFiber } from "../core/helpers/execution-context";
|
|
3
|
-
import {
|
|
3
|
+
import { useReducerWithDerivedState } from "./useReducer";
|
|
4
4
|
import { depsShallowEqual } from "./utils/depsShallowEqual";
|
|
5
5
|
|
|
6
6
|
const memoReducer = () => {
|
|
@@ -9,9 +9,9 @@ const memoReducer = () => {
|
|
|
9
9
|
|
|
10
10
|
type MemoState<T> = { value: T; deps: readonly unknown[] };
|
|
11
11
|
|
|
12
|
-
export const
|
|
12
|
+
export const useMemo = <T>(fn: () => T, deps: readonly unknown[]): T => {
|
|
13
13
|
const fiber = getCurrentResourceFiber();
|
|
14
|
-
const [state] =
|
|
14
|
+
const [state] = useReducerWithDerivedState(
|
|
15
15
|
memoReducer,
|
|
16
16
|
(state: MemoState<T> | null): MemoState<T> => {
|
|
17
17
|
if (state && depsShallowEqual(state.deps, deps)) return state;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useReducerWithDerivedState } from "./useReducer";
|
|
2
|
+
|
|
3
|
+
const MEMO_CACHE_SENTINEL = Symbol.for("react.memo_cache_sentinel");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Backs React Compiler's memo cache. Compiled output allocates it through
|
|
7
|
+
* `react/compiler-runtime`'s `c(size)` (`ReactSharedInternals.H.useMemoCache(size)`);
|
|
8
|
+
* a tap ref persists the array so compiled resources run without `"use no memo"`.
|
|
9
|
+
*/
|
|
10
|
+
export const useMemoCache = (size: number): unknown[] => {
|
|
11
|
+
// clone the memo value once per render
|
|
12
|
+
let cloned = false;
|
|
13
|
+
const [cache] = useReducerWithDerivedState(
|
|
14
|
+
() => [] as unknown[],
|
|
15
|
+
(arr) => {
|
|
16
|
+
if (cloned) return arr;
|
|
17
|
+
cloned = true;
|
|
18
|
+
return [...arr];
|
|
19
|
+
},
|
|
20
|
+
size,
|
|
21
|
+
(length) => Array.from({ length }).fill(MEMO_CACHE_SENTINEL),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return cache;
|
|
25
|
+
};
|
|
@@ -2,7 +2,7 @@ import { isDevelopment } from "../core/helpers/env";
|
|
|
2
2
|
import { getCurrentResourceFiber } from "../core/helpers/execution-context";
|
|
3
3
|
import type { ReducerQueueEntry, ResourceFiber } from "../core/types";
|
|
4
4
|
import { markCellDirty } from "../core/helpers/root";
|
|
5
|
-
import {
|
|
5
|
+
import { useCell } from "./utils/useCell";
|
|
6
6
|
|
|
7
7
|
type Dispatch<A> = (action: A) => void;
|
|
8
8
|
|
|
@@ -28,13 +28,13 @@ const dispatchOnFiber = (
|
|
|
28
28
|
});
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
function
|
|
31
|
+
function useReducerImpl<S, A, I, R extends S>(
|
|
32
32
|
reducer: (state: S, action: A) => S,
|
|
33
33
|
getDerivedState: ((state: S) => R) | undefined,
|
|
34
34
|
initialArg: S | I,
|
|
35
35
|
initFn: ((arg: I) => S) | undefined,
|
|
36
36
|
): [R, Dispatch<A>] {
|
|
37
|
-
const cell =
|
|
37
|
+
const cell = useCell("reducer", () => {
|
|
38
38
|
const fiber = getCurrentResourceFiber();
|
|
39
39
|
|
|
40
40
|
// First render: compute initial state
|
|
@@ -105,21 +105,21 @@ function tapReducerImpl<S, A, I, R extends S>(
|
|
|
105
105
|
return [cell.workInProgress, cell.dispatch];
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
export function
|
|
108
|
+
export function useReducer<S, A>(
|
|
109
109
|
reducer: (state: S, action: A) => S,
|
|
110
110
|
initialState: S,
|
|
111
111
|
): [S, Dispatch<A>];
|
|
112
|
-
export function
|
|
112
|
+
export function useReducer<S, A, I>(
|
|
113
113
|
reducer: (state: S, action: A) => S,
|
|
114
114
|
initialArg: I,
|
|
115
115
|
init: (arg: I) => S,
|
|
116
116
|
): [S, Dispatch<A>];
|
|
117
|
-
export function
|
|
117
|
+
export function useReducer<S, A, I>(
|
|
118
118
|
reducer: (state: S, action: A) => S,
|
|
119
119
|
initialArg: S | I,
|
|
120
120
|
init?: (arg: I) => S,
|
|
121
121
|
): [S, Dispatch<A>] {
|
|
122
|
-
return
|
|
122
|
+
return useReducerImpl(
|
|
123
123
|
reducer,
|
|
124
124
|
undefined,
|
|
125
125
|
initialArg as S,
|
|
@@ -127,22 +127,34 @@ export function tapReducer<S, A, I>(
|
|
|
127
127
|
);
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
|
|
130
|
+
/**
|
|
131
|
+
* @deprecated experimental — a `getDerivedStateFromProps` replacement for
|
|
132
|
+
* resources: adjust state in response to props without setting during render.
|
|
133
|
+
* Tap-only for now (call it inside a resource render, not a React component) and
|
|
134
|
+
* may change before stabilizing.
|
|
135
|
+
*/
|
|
136
|
+
export function useReducerWithDerivedState<S, A, R extends S>(
|
|
131
137
|
reducer: (state: S, action: A) => S,
|
|
132
138
|
getDerivedState: (state: S) => R,
|
|
133
139
|
initialState: S,
|
|
134
140
|
): [R, Dispatch<A>];
|
|
135
|
-
|
|
141
|
+
/**
|
|
142
|
+
* @deprecated experimental — a `getDerivedStateFromProps` replacement for
|
|
143
|
+
* resources: adjust state in response to props without setting during render.
|
|
144
|
+
* Tap-only for now (call it inside a resource render, not a React component) and
|
|
145
|
+
* may change before stabilizing.
|
|
146
|
+
*/
|
|
147
|
+
export function useReducerWithDerivedState<S, A, I, R extends S>(
|
|
136
148
|
reducer: (state: S, action: A) => S,
|
|
137
149
|
getDerivedState: (state: S) => R,
|
|
138
150
|
initialArg: I,
|
|
139
151
|
init: (arg: I) => S,
|
|
140
152
|
): [R, Dispatch<A>];
|
|
141
|
-
export function
|
|
153
|
+
export function useReducerWithDerivedState<S, A, I, R extends S>(
|
|
142
154
|
reducer: (state: S, action: A) => S,
|
|
143
155
|
getDerivedState: (state: S) => R,
|
|
144
156
|
initialArg: I,
|
|
145
157
|
init?: (arg: I) => S,
|
|
146
158
|
): [R, Dispatch<A>] {
|
|
147
|
-
return
|
|
159
|
+
return useReducerImpl(reducer, getDerivedState, initialArg, init);
|
|
148
160
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useState } from "./useState";
|
|
2
|
+
|
|
3
|
+
export namespace useRef {
|
|
4
|
+
export interface RefObject<T> {
|
|
5
|
+
current: T;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function useRef<T>(initialValue: T): useRef.RefObject<T>;
|
|
10
|
+
export function useRef<T = undefined>(): useRef.RefObject<T | undefined>;
|
|
11
|
+
export function useRef<T>(initialValue?: T): useRef.RefObject<T | undefined> {
|
|
12
|
+
const [state] = useState(() => ({
|
|
13
|
+
current: initialValue,
|
|
14
|
+
}));
|
|
15
|
+
return state;
|
|
16
|
+
}
|
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
import type { ExtractResourceReturnType, ResourceElement } from "../core/types";
|
|
2
|
-
import {
|
|
2
|
+
import { useEffect } from "./useEffect";
|
|
3
3
|
import {
|
|
4
4
|
createResourceFiber,
|
|
5
5
|
unmountResourceFiber,
|
|
6
6
|
renderResourceFiber,
|
|
7
7
|
commitResourceFiber,
|
|
8
8
|
} from "../core/ResourceFiber";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { useMemo } from "./useMemo";
|
|
10
|
+
import { useRef } from "./useRef";
|
|
11
11
|
import { getCurrentResourceFiber } from "../core/helpers/execution-context";
|
|
12
12
|
|
|
13
|
-
export function
|
|
13
|
+
export function useResource<E extends ResourceElement<any, any>>(
|
|
14
14
|
element: E,
|
|
15
15
|
): ExtractResourceReturnType<E>;
|
|
16
|
-
export function
|
|
16
|
+
export function useResource<E extends ResourceElement<any, any>>(
|
|
17
17
|
element: E,
|
|
18
18
|
propsDeps: readonly unknown[],
|
|
19
19
|
): ExtractResourceReturnType<E>;
|
|
20
|
-
export function
|
|
20
|
+
export function useResource<E extends ResourceElement<any, any>>(
|
|
21
21
|
element: E,
|
|
22
22
|
propsDeps?: readonly unknown[],
|
|
23
23
|
): ExtractResourceReturnType<E> {
|
|
24
24
|
const parentFiber = getCurrentResourceFiber();
|
|
25
|
-
const versionRef =
|
|
26
|
-
const fiber =
|
|
25
|
+
const versionRef = useRef(0);
|
|
26
|
+
const fiber = useMemo(() => {
|
|
27
27
|
void element.key;
|
|
28
28
|
return createResourceFiber(element.type, parentFiber.root, () => {
|
|
29
29
|
versionRef.current++;
|
|
@@ -32,15 +32,16 @@ export function tapResource<E extends ResourceElement<any, any>>(
|
|
|
32
32
|
}, [element.type, element.key, parentFiber]);
|
|
33
33
|
|
|
34
34
|
const result = propsDeps
|
|
35
|
-
?
|
|
35
|
+
? // oxlint-disable-next-line react/rules-of-hooks -- propsDeps presence is fixed per call site, so the conditional call order is stable
|
|
36
|
+
useMemo(
|
|
36
37
|
() => renderResourceFiber(fiber, element.props),
|
|
37
|
-
// oxlint-disable-next-line
|
|
38
|
+
// oxlint-disable-next-line react/exhaustive-deps -- props identity replaced by user-provided deps
|
|
38
39
|
[fiber, ...propsDeps, versionRef.current],
|
|
39
40
|
)
|
|
40
41
|
: renderResourceFiber(fiber, element.props);
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
useEffect(() => () => unmountResourceFiber(fiber), [fiber]);
|
|
44
|
+
useEffect(() => {
|
|
44
45
|
commitResourceFiber(fiber, result);
|
|
45
46
|
}, [fiber, result]);
|
|
46
47
|
|