@tldraw/state-react 3.16.0-internal.a478398270c6 → 3.16.0-internal.f8b97f0c414f
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/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/useReactor.js +8 -13
- package/dist-cjs/lib/useReactor.js.map +2 -2
- package/dist-cjs/lib/useValue.js +17 -33
- package/dist-cjs/lib/useValue.js.map +3 -3
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/useReactor.mjs +9 -14
- package/dist-esm/lib/useReactor.mjs.map +2 -2
- package/dist-esm/lib/useValue.mjs +18 -34
- package/dist-esm/lib/useValue.mjs.map +3 -3
- package/package.json +12 -20
- package/src/lib/useComputed.test.tsx +2 -1
- package/src/lib/useReactor.ts +11 -17
- package/src/lib/useValue.test.tsx +90 -4
- package/src/lib/useValue.ts +19 -44
package/dist-cjs/index.js
CHANGED
|
@@ -37,7 +37,7 @@ var import_useStateTracking = require("./lib/useStateTracking");
|
|
|
37
37
|
var import_useValue = require("./lib/useValue");
|
|
38
38
|
(0, import_utils.registerTldrawLibraryVersion)(
|
|
39
39
|
"@tldraw/state-react",
|
|
40
|
-
"3.16.0-internal.
|
|
40
|
+
"3.16.0-internal.f8b97f0c414f",
|
|
41
41
|
"cjs"
|
|
42
42
|
);
|
|
43
43
|
//# sourceMappingURL=index.js.map
|
|
@@ -22,27 +22,22 @@ __export(useReactor_exports, {
|
|
|
22
22
|
});
|
|
23
23
|
module.exports = __toCommonJS(useReactor_exports);
|
|
24
24
|
var import_state = require("@tldraw/state");
|
|
25
|
+
var import_utils = require("@tldraw/utils");
|
|
25
26
|
var import_react = require("react");
|
|
26
27
|
function useReactor(name, reactFn, deps = []) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
(0, import_react.useEffect)(() => {
|
|
29
|
+
let cancelFn;
|
|
30
|
+
const scheduler = new import_state.EffectScheduler(name, reactFn, {
|
|
30
31
|
scheduleEffect: (cb) => {
|
|
31
|
-
|
|
32
|
-
raf.current = rafId;
|
|
33
|
-
return rafId;
|
|
32
|
+
cancelFn = (0, import_utils.throttleToNextFrame)(cb);
|
|
34
33
|
}
|
|
35
|
-
})
|
|
36
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
37
|
-
deps
|
|
38
|
-
);
|
|
39
|
-
(0, import_react.useEffect)(() => {
|
|
34
|
+
});
|
|
40
35
|
scheduler.attach();
|
|
41
36
|
scheduler.execute();
|
|
42
37
|
return () => {
|
|
43
38
|
scheduler.detach();
|
|
44
|
-
|
|
39
|
+
cancelFn?.();
|
|
45
40
|
};
|
|
46
|
-
},
|
|
41
|
+
}, deps);
|
|
47
42
|
}
|
|
48
43
|
//# sourceMappingURL=useReactor.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/useReactor.ts"],
|
|
4
|
-
"sourcesContent": ["import { EffectScheduler } from '@tldraw/state'\nimport {
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAgC;AAChC,
|
|
4
|
+
"sourcesContent": ["import { EffectScheduler } from '@tldraw/state'\nimport { throttleToNextFrame } from '@tldraw/utils'\nimport { useEffect } from 'react'\n\n/** @public */\nexport function useReactor(name: string, reactFn: () => void, deps: undefined | any[] = []) {\n\tuseEffect(() => {\n\t\tlet cancelFn: () => void | undefined\n\t\tconst scheduler = new EffectScheduler(name, reactFn, {\n\t\t\tscheduleEffect: (cb) => {\n\t\t\t\tcancelFn = throttleToNextFrame(cb)\n\t\t\t},\n\t\t})\n\t\tscheduler.attach()\n\t\tscheduler.execute()\n\t\treturn () => {\n\t\t\tscheduler.detach()\n\t\t\tcancelFn?.()\n\t\t}\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t}, deps)\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAgC;AAChC,mBAAoC;AACpC,mBAA0B;AAGnB,SAAS,WAAW,MAAc,SAAqB,OAA0B,CAAC,GAAG;AAC3F,8BAAU,MAAM;AACf,QAAI;AACJ,UAAM,YAAY,IAAI,6BAAgB,MAAM,SAAS;AAAA,MACpD,gBAAgB,CAAC,OAAO;AACvB,uBAAW,kCAAoB,EAAE;AAAA,MAClC;AAAA,IACD,CAAC;AACD,cAAU,OAAO;AACjB,cAAU,QAAQ;AAClB,WAAO,MAAM;AACZ,gBAAU,OAAO;AACjB,iBAAW;AAAA,IACZ;AAAA,EAED,GAAG,IAAI;AACR;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist-cjs/lib/useValue.js
CHANGED
|
@@ -27,39 +27,23 @@ function useValue() {
|
|
|
27
27
|
const args = arguments;
|
|
28
28
|
const deps = args.length === 3 ? args[2] : [args[0]];
|
|
29
29
|
const name = args.length === 3 ? args[0] : `useValue(${args[0].name})`;
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
});
|
|
30
|
+
const { $val, subscribe, getSnapshot } = (0, import_react.useMemo)(() => {
|
|
31
|
+
const $val2 = args.length === 1 ? args[0] : (0, import_state.computed)(name, args[1]);
|
|
32
|
+
return {
|
|
33
|
+
$val: $val2,
|
|
34
|
+
subscribe: (notify) => {
|
|
35
|
+
return (0, import_state.react)(`useValue(${name})`, () => {
|
|
36
|
+
try {
|
|
37
|
+
$val2.get();
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
notify();
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
getSnapshot: () => $val2.lastChangedEpoch
|
|
44
|
+
};
|
|
47
45
|
}, deps);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
subscribe: (listen) => {
|
|
52
|
-
return (0, import_state.react)(`useValue(${name})`, () => {
|
|
53
|
-
$val.get();
|
|
54
|
-
listen();
|
|
55
|
-
});
|
|
56
|
-
},
|
|
57
|
-
getSnapshot: () => $val.get()
|
|
58
|
-
};
|
|
59
|
-
}, [$val]);
|
|
60
|
-
return (0, import_react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
|
|
61
|
-
} finally {
|
|
62
|
-
isInRender.current = false;
|
|
63
|
-
}
|
|
46
|
+
(0, import_react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
|
|
47
|
+
return $val.__unsafe__getWithoutCapture();
|
|
64
48
|
}
|
|
65
49
|
//# sourceMappingURL=useValue.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/useValue.ts"],
|
|
4
|
-
"sourcesContent": ["/* eslint-disable prefer-rest-params */\nimport { Signal, computed, react } from '@tldraw/state'\nimport { useMemo,
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,mBAAwC;AACxC,
|
|
6
|
-
"names": []
|
|
4
|
+
"sourcesContent": ["/* eslint-disable prefer-rest-params */\nimport { Signal, computed, react } from '@tldraw/state'\nimport { useMemo, useSyncExternalStore } from 'react'\n\n/** @public */\nexport function useValue<Value>(value: Signal<Value>): Value\n\n/**\n * Extracts the value from a signal and subscribes to it.\n *\n * Note that you do not need to use this hook if you are wrapping the component with {@link track}\n *\n * @example\n * ```ts\n * const Counter: React.FC = () => {\n * const $count = useAtom('count', 0)\n * const increment = useCallback(() => $count.set($count.get() + 1), [count])\n * const currentCount = useValue($count)\n * return <button onClick={increment}>{currentCount}</button>\n * }\n * ```\n *\n * You can also pass a function to compute the value and it will be memoized as in `useComputed`:\n *\n * @example\n * ```ts\n * type GreeterProps = {\n * firstName: Signal<string>\n * lastName: Signal<string>\n * }\n *\n * const Greeter = track(function Greeter({ firstName, lastName }: GreeterProps) {\n * const fullName = useValue('fullName', () => `${firstName.get()} ${lastName.get()}`, [\n * firstName,\n * lastName,\n * ])\n * return <div>Hello {fullName}!</div>\n * })\n * ```\n *\n * @public\n */\nexport function useValue<Value>(name: string, fn: () => Value, deps: unknown[]): Value\n\n/** @public */\nexport function useValue() {\n\tconst args = arguments\n\t// deps will be either the computed or the deps array\n\tconst deps = args.length === 3 ? args[2] : [args[0]]\n\tconst name = args.length === 3 ? args[0] : `useValue(${args[0].name})`\n\n\tconst { $val, subscribe, getSnapshot } = useMemo(() => {\n\t\tconst $val =\n\t\t\targs.length === 1 ? (args[0] as Signal<any>) : (computed(name, args[1]) as Signal<any>)\n\n\t\treturn {\n\t\t\t$val,\n\t\t\tsubscribe: (notify: () => void) => {\n\t\t\t\treturn react(`useValue(${name})`, () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\t$val.get()\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Will be rethrown during render if the component doesn't unmount first.\n\t\t\t\t\t}\n\t\t\t\t\tnotify()\n\t\t\t\t})\n\t\t\t},\n\t\t\tgetSnapshot: () => $val.lastChangedEpoch,\n\t\t}\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t}, deps)\n\n\tuseSyncExternalStore(subscribe, getSnapshot, getSnapshot)\n\treturn $val.__unsafe__getWithoutCapture()\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,mBAAwC;AACxC,mBAA8C;AA2CvC,SAAS,WAAW;AAC1B,QAAM,OAAO;AAEb,QAAM,OAAO,KAAK,WAAW,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACnD,QAAM,OAAO,KAAK,WAAW,IAAI,KAAK,CAAC,IAAI,YAAY,KAAK,CAAC,EAAE,IAAI;AAEnE,QAAM,EAAE,MAAM,WAAW,YAAY,QAAI,sBAAQ,MAAM;AACtD,UAAMA,QACL,KAAK,WAAW,IAAK,KAAK,CAAC,QAAqB,uBAAS,MAAM,KAAK,CAAC,CAAC;AAEvE,WAAO;AAAA,MACN,MAAAA;AAAA,MACA,WAAW,CAAC,WAAuB;AAClC,mBAAO,oBAAM,YAAY,IAAI,KAAK,MAAM;AACvC,cAAI;AACH,YAAAA,MAAK,IAAI;AAAA,UACV,QAAQ;AAAA,UAER;AACA,iBAAO;AAAA,QACR,CAAC;AAAA,MACF;AAAA,MACA,aAAa,MAAMA,MAAK;AAAA,IACzB;AAAA,EAED,GAAG,IAAI;AAEP,yCAAqB,WAAW,aAAa,WAAW;AACxD,SAAO,KAAK,4BAA4B;AACzC;",
|
|
6
|
+
"names": ["$val"]
|
|
7
7
|
}
|
package/dist-esm/index.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { useStateTracking } from "./lib/useStateTracking.mjs";
|
|
|
8
8
|
import { useValue } from "./lib/useValue.mjs";
|
|
9
9
|
registerTldrawLibraryVersion(
|
|
10
10
|
"@tldraw/state-react",
|
|
11
|
-
"3.16.0-internal.
|
|
11
|
+
"3.16.0-internal.f8b97f0c414f",
|
|
12
12
|
"esm"
|
|
13
13
|
);
|
|
14
14
|
export {
|
|
@@ -1,26 +1,21 @@
|
|
|
1
1
|
import { EffectScheduler } from "@tldraw/state";
|
|
2
|
-
import {
|
|
2
|
+
import { throttleToNextFrame } from "@tldraw/utils";
|
|
3
|
+
import { useEffect } from "react";
|
|
3
4
|
function useReactor(name, reactFn, deps = []) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
let cancelFn;
|
|
7
|
+
const scheduler = new EffectScheduler(name, reactFn, {
|
|
7
8
|
scheduleEffect: (cb) => {
|
|
8
|
-
|
|
9
|
-
raf.current = rafId;
|
|
10
|
-
return rafId;
|
|
9
|
+
cancelFn = throttleToNextFrame(cb);
|
|
11
10
|
}
|
|
12
|
-
})
|
|
13
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
14
|
-
deps
|
|
15
|
-
);
|
|
16
|
-
useEffect(() => {
|
|
11
|
+
});
|
|
17
12
|
scheduler.attach();
|
|
18
13
|
scheduler.execute();
|
|
19
14
|
return () => {
|
|
20
15
|
scheduler.detach();
|
|
21
|
-
|
|
16
|
+
cancelFn?.();
|
|
22
17
|
};
|
|
23
|
-
},
|
|
18
|
+
}, deps);
|
|
24
19
|
}
|
|
25
20
|
export {
|
|
26
21
|
useReactor
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/useReactor.ts"],
|
|
4
|
-
"sourcesContent": ["import { EffectScheduler } from '@tldraw/state'\nimport {
|
|
5
|
-
"mappings": "AAAA,SAAS,uBAAuB;AAChC,SAAS,
|
|
4
|
+
"sourcesContent": ["import { EffectScheduler } from '@tldraw/state'\nimport { throttleToNextFrame } from '@tldraw/utils'\nimport { useEffect } from 'react'\n\n/** @public */\nexport function useReactor(name: string, reactFn: () => void, deps: undefined | any[] = []) {\n\tuseEffect(() => {\n\t\tlet cancelFn: () => void | undefined\n\t\tconst scheduler = new EffectScheduler(name, reactFn, {\n\t\t\tscheduleEffect: (cb) => {\n\t\t\t\tcancelFn = throttleToNextFrame(cb)\n\t\t\t},\n\t\t})\n\t\tscheduler.attach()\n\t\tscheduler.execute()\n\t\treturn () => {\n\t\t\tscheduler.detach()\n\t\t\tcancelFn?.()\n\t\t}\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t}, deps)\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,uBAAuB;AAChC,SAAS,2BAA2B;AACpC,SAAS,iBAAiB;AAGnB,SAAS,WAAW,MAAc,SAAqB,OAA0B,CAAC,GAAG;AAC3F,YAAU,MAAM;AACf,QAAI;AACJ,UAAM,YAAY,IAAI,gBAAgB,MAAM,SAAS;AAAA,MACpD,gBAAgB,CAAC,OAAO;AACvB,mBAAW,oBAAoB,EAAE;AAAA,MAClC;AAAA,IACD,CAAC;AACD,cAAU,OAAO;AACjB,cAAU,QAAQ;AAClB,WAAO,MAAM;AACZ,gBAAU,OAAO;AACjB,iBAAW;AAAA,IACZ;AAAA,EAED,GAAG,IAAI;AACR;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,43 +1,27 @@
|
|
|
1
1
|
import { computed, react } from "@tldraw/state";
|
|
2
|
-
import { useMemo,
|
|
2
|
+
import { useMemo, useSyncExternalStore } from "react";
|
|
3
3
|
function useValue() {
|
|
4
4
|
const args = arguments;
|
|
5
5
|
const deps = args.length === 3 ? args[2] : [args[0]];
|
|
6
6
|
const name = args.length === 3 ? args[0] : `useValue(${args[0].name})`;
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
});
|
|
7
|
+
const { $val, subscribe, getSnapshot } = useMemo(() => {
|
|
8
|
+
const $val2 = args.length === 1 ? args[0] : computed(name, args[1]);
|
|
9
|
+
return {
|
|
10
|
+
$val: $val2,
|
|
11
|
+
subscribe: (notify) => {
|
|
12
|
+
return react(`useValue(${name})`, () => {
|
|
13
|
+
try {
|
|
14
|
+
$val2.get();
|
|
15
|
+
} catch {
|
|
16
|
+
}
|
|
17
|
+
notify();
|
|
18
|
+
});
|
|
19
|
+
},
|
|
20
|
+
getSnapshot: () => $val2.lastChangedEpoch
|
|
21
|
+
};
|
|
24
22
|
}, deps);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return {
|
|
28
|
-
subscribe: (listen) => {
|
|
29
|
-
return react(`useValue(${name})`, () => {
|
|
30
|
-
$val.get();
|
|
31
|
-
listen();
|
|
32
|
-
});
|
|
33
|
-
},
|
|
34
|
-
getSnapshot: () => $val.get()
|
|
35
|
-
};
|
|
36
|
-
}, [$val]);
|
|
37
|
-
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
38
|
-
} finally {
|
|
39
|
-
isInRender.current = false;
|
|
40
|
-
}
|
|
23
|
+
useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
24
|
+
return $val.__unsafe__getWithoutCapture();
|
|
41
25
|
}
|
|
42
26
|
export {
|
|
43
27
|
useValue
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/useValue.ts"],
|
|
4
|
-
"sourcesContent": ["/* eslint-disable prefer-rest-params */\nimport { Signal, computed, react } from '@tldraw/state'\nimport { useMemo,
|
|
5
|
-
"mappings": "AACA,SAAiB,UAAU,aAAa;AACxC,SAAS,SAAS,
|
|
6
|
-
"names": []
|
|
4
|
+
"sourcesContent": ["/* eslint-disable prefer-rest-params */\nimport { Signal, computed, react } from '@tldraw/state'\nimport { useMemo, useSyncExternalStore } from 'react'\n\n/** @public */\nexport function useValue<Value>(value: Signal<Value>): Value\n\n/**\n * Extracts the value from a signal and subscribes to it.\n *\n * Note that you do not need to use this hook if you are wrapping the component with {@link track}\n *\n * @example\n * ```ts\n * const Counter: React.FC = () => {\n * const $count = useAtom('count', 0)\n * const increment = useCallback(() => $count.set($count.get() + 1), [count])\n * const currentCount = useValue($count)\n * return <button onClick={increment}>{currentCount}</button>\n * }\n * ```\n *\n * You can also pass a function to compute the value and it will be memoized as in `useComputed`:\n *\n * @example\n * ```ts\n * type GreeterProps = {\n * firstName: Signal<string>\n * lastName: Signal<string>\n * }\n *\n * const Greeter = track(function Greeter({ firstName, lastName }: GreeterProps) {\n * const fullName = useValue('fullName', () => `${firstName.get()} ${lastName.get()}`, [\n * firstName,\n * lastName,\n * ])\n * return <div>Hello {fullName}!</div>\n * })\n * ```\n *\n * @public\n */\nexport function useValue<Value>(name: string, fn: () => Value, deps: unknown[]): Value\n\n/** @public */\nexport function useValue() {\n\tconst args = arguments\n\t// deps will be either the computed or the deps array\n\tconst deps = args.length === 3 ? args[2] : [args[0]]\n\tconst name = args.length === 3 ? args[0] : `useValue(${args[0].name})`\n\n\tconst { $val, subscribe, getSnapshot } = useMemo(() => {\n\t\tconst $val =\n\t\t\targs.length === 1 ? (args[0] as Signal<any>) : (computed(name, args[1]) as Signal<any>)\n\n\t\treturn {\n\t\t\t$val,\n\t\t\tsubscribe: (notify: () => void) => {\n\t\t\t\treturn react(`useValue(${name})`, () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\t$val.get()\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Will be rethrown during render if the component doesn't unmount first.\n\t\t\t\t\t}\n\t\t\t\t\tnotify()\n\t\t\t\t})\n\t\t\t},\n\t\t\tgetSnapshot: () => $val.lastChangedEpoch,\n\t\t}\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t}, deps)\n\n\tuseSyncExternalStore(subscribe, getSnapshot, getSnapshot)\n\treturn $val.__unsafe__getWithoutCapture()\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAiB,UAAU,aAAa;AACxC,SAAS,SAAS,4BAA4B;AA2CvC,SAAS,WAAW;AAC1B,QAAM,OAAO;AAEb,QAAM,OAAO,KAAK,WAAW,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACnD,QAAM,OAAO,KAAK,WAAW,IAAI,KAAK,CAAC,IAAI,YAAY,KAAK,CAAC,EAAE,IAAI;AAEnE,QAAM,EAAE,MAAM,WAAW,YAAY,IAAI,QAAQ,MAAM;AACtD,UAAMA,QACL,KAAK,WAAW,IAAK,KAAK,CAAC,IAAqB,SAAS,MAAM,KAAK,CAAC,CAAC;AAEvE,WAAO;AAAA,MACN,MAAAA;AAAA,MACA,WAAW,CAAC,WAAuB;AAClC,eAAO,MAAM,YAAY,IAAI,KAAK,MAAM;AACvC,cAAI;AACH,YAAAA,MAAK,IAAI;AAAA,UACV,QAAQ;AAAA,UAER;AACA,iBAAO;AAAA,QACR,CAAC;AAAA,MACF;AAAA,MACA,aAAa,MAAMA,MAAK;AAAA,IACzB;AAAA,EAED,GAAG,IAAI;AAEP,uBAAqB,WAAW,aAAa,WAAW;AACxD,SAAO,KAAK,4BAA4B;AACzC;",
|
|
6
|
+
"names": ["$val"]
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tldraw/state-react",
|
|
3
|
-
"description": "
|
|
4
|
-
"version": "3.16.0-internal.
|
|
3
|
+
"description": "tldraw infinite canvas SDK (react bindings for state).",
|
|
4
|
+
"version": "3.16.0-internal.f8b97f0c414f",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "tldraw Inc.",
|
|
7
7
|
"email": "hello@tldraw.com"
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
"keywords": [
|
|
19
19
|
"tldraw",
|
|
20
|
+
"sdk",
|
|
20
21
|
"drawing",
|
|
21
22
|
"app",
|
|
22
23
|
"development",
|
|
@@ -31,38 +32,29 @@
|
|
|
31
32
|
"src"
|
|
32
33
|
],
|
|
33
34
|
"scripts": {
|
|
34
|
-
"test-ci": "
|
|
35
|
-
"test": "yarn run -T
|
|
36
|
-
"test-coverage": "
|
|
35
|
+
"test-ci": "yarn run -T vitest run --passWithNoTests",
|
|
36
|
+
"test": "yarn run -T vitest --passWithNoTests",
|
|
37
|
+
"test-coverage": "yarn run -T vitest run --coverage --passWithNoTests",
|
|
37
38
|
"build": "yarn run -T tsx ../../internal/scripts/build-package.ts",
|
|
38
39
|
"build-api": "yarn run -T tsx ../../internal/scripts/build-api.ts",
|
|
39
40
|
"prepack": "yarn run -T tsx ../../internal/scripts/prepack.ts",
|
|
40
41
|
"postpack": "../../internal/scripts/postpack.sh",
|
|
41
42
|
"pack-tarball": "yarn pack",
|
|
42
|
-
"lint": "yarn run -T tsx ../../internal/scripts/lint.ts"
|
|
43
|
-
|
|
44
|
-
"jest": {
|
|
45
|
-
"preset": "../../internal/config/jest/node/jest-preset.js",
|
|
46
|
-
"setupFiles": [
|
|
47
|
-
"raf/polyfill"
|
|
48
|
-
],
|
|
49
|
-
"moduleNameMapper": {
|
|
50
|
-
"^~(.*)": "<rootDir>/src/$1"
|
|
51
|
-
},
|
|
52
|
-
"testEnvironment": "jsdom"
|
|
43
|
+
"lint": "yarn run -T tsx ../../internal/scripts/lint.ts",
|
|
44
|
+
"context": "yarn run -T tsx ../../internal/scripts/context.ts"
|
|
53
45
|
},
|
|
54
46
|
"dependencies": {
|
|
55
|
-
"@tldraw/state": "3.16.0-internal.
|
|
56
|
-
"@tldraw/utils": "3.16.0-internal.
|
|
47
|
+
"@tldraw/state": "3.16.0-internal.f8b97f0c414f",
|
|
48
|
+
"@tldraw/utils": "3.16.0-internal.f8b97f0c414f"
|
|
57
49
|
},
|
|
58
50
|
"devDependencies": {
|
|
59
|
-
"@testing-library/jest-dom": "^5.17.0",
|
|
60
51
|
"@testing-library/react": "^15.0.7",
|
|
61
52
|
"@types/lodash": "^4.17.14",
|
|
62
53
|
"@types/react": "^18.3.18",
|
|
63
54
|
"lodash": "^4.17.21",
|
|
64
55
|
"react": "^18.3.1",
|
|
65
|
-
"react-dom": "^18.3.1"
|
|
56
|
+
"react-dom": "^18.3.1",
|
|
57
|
+
"vitest": "^3.2.4"
|
|
66
58
|
},
|
|
67
59
|
"peerDependencies": {
|
|
68
60
|
"react": "^18.2.0 || ^19.0.0",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { act, render, RenderResult } from '@testing-library/react'
|
|
2
2
|
import { Atom, Computed } from '@tldraw/state'
|
|
3
3
|
import { useState } from 'react'
|
|
4
|
+
import { vi } from 'vitest'
|
|
4
5
|
import { useAtom } from './useAtom'
|
|
5
6
|
import { useComputed } from './useComputed'
|
|
6
7
|
import { useValue } from './useValue'
|
|
@@ -76,7 +77,7 @@ test('useComputed allows optionally passing options', async () => {
|
|
|
76
77
|
let theComputed = null as null | Computed<number>
|
|
77
78
|
let theAtom = null as null | Atom<number>
|
|
78
79
|
let setCount = null as null | ((count: number) => void)
|
|
79
|
-
const isEqual =
|
|
80
|
+
const isEqual = vi.fn((a, b) => a === b)
|
|
80
81
|
function Component() {
|
|
81
82
|
const [count, _setCount] = useState(0)
|
|
82
83
|
setCount = _setCount
|
package/src/lib/useReactor.ts
CHANGED
|
@@ -1,28 +1,22 @@
|
|
|
1
1
|
import { EffectScheduler } from '@tldraw/state'
|
|
2
|
-
import {
|
|
2
|
+
import { throttleToNextFrame } from '@tldraw/utils'
|
|
3
|
+
import { useEffect } from 'react'
|
|
3
4
|
|
|
4
5
|
/** @public */
|
|
5
6
|
export function useReactor(name: string, reactFn: () => void, deps: undefined | any[] = []) {
|
|
6
|
-
const raf = useRef(-1)
|
|
7
|
-
const scheduler = useMemo(
|
|
8
|
-
() =>
|
|
9
|
-
new EffectScheduler(name, reactFn, {
|
|
10
|
-
scheduleEffect: (cb) => {
|
|
11
|
-
const rafId = requestAnimationFrame(cb)
|
|
12
|
-
raf.current = rafId
|
|
13
|
-
return rafId
|
|
14
|
-
},
|
|
15
|
-
}),
|
|
16
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
17
|
-
deps
|
|
18
|
-
)
|
|
19
|
-
|
|
20
7
|
useEffect(() => {
|
|
8
|
+
let cancelFn: () => void | undefined
|
|
9
|
+
const scheduler = new EffectScheduler(name, reactFn, {
|
|
10
|
+
scheduleEffect: (cb) => {
|
|
11
|
+
cancelFn = throttleToNextFrame(cb)
|
|
12
|
+
},
|
|
13
|
+
})
|
|
21
14
|
scheduler.attach()
|
|
22
15
|
scheduler.execute()
|
|
23
16
|
return () => {
|
|
24
17
|
scheduler.detach()
|
|
25
|
-
|
|
18
|
+
cancelFn?.()
|
|
26
19
|
}
|
|
27
|
-
|
|
20
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
21
|
+
}, deps)
|
|
28
22
|
}
|
|
@@ -1,10 +1,37 @@
|
|
|
1
1
|
import { RenderResult, act, render } from '@testing-library/react'
|
|
2
2
|
import { Atom, Computed, atom } from '@tldraw/state'
|
|
3
|
-
import { useState } from 'react'
|
|
3
|
+
import { Component, ReactNode, useState } from 'react'
|
|
4
|
+
import { vi } from 'vitest'
|
|
4
5
|
import { useAtom } from './useAtom'
|
|
5
6
|
import { useComputed } from './useComputed'
|
|
6
7
|
import { useValue } from './useValue'
|
|
7
8
|
|
|
9
|
+
// Error boundary component for testing
|
|
10
|
+
class TestErrorBoundary extends Component<
|
|
11
|
+
{ children: ReactNode; onError?(error: Error): void },
|
|
12
|
+
{ hasError: boolean; error: Error | null }
|
|
13
|
+
> {
|
|
14
|
+
constructor(props: { children: ReactNode; onError?(error: Error): void }) {
|
|
15
|
+
super(props)
|
|
16
|
+
this.state = { hasError: false, error: null }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static getDerivedStateFromError(error: Error) {
|
|
20
|
+
return { hasError: true, error }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
override componentDidCatch(error: Error) {
|
|
24
|
+
this.props.onError?.(error)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
override render() {
|
|
28
|
+
if (this.state.hasError) {
|
|
29
|
+
return <div data-testid="error-boundary">Error: {this.state.error?.message}</div>
|
|
30
|
+
}
|
|
31
|
+
return this.props.children
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
8
35
|
test('useValue returns a value from a computed', async () => {
|
|
9
36
|
let theComputed = null as null | Computed<number>
|
|
10
37
|
let theAtom = null as null | Atom<number>
|
|
@@ -85,6 +112,7 @@ test('useValue returns a value from a compute function', async () => {
|
|
|
85
112
|
|
|
86
113
|
test("useValue doesn't throw when used in a zombie-child component", async () => {
|
|
87
114
|
const theAtom = atom<Record<string, number>>('map', { a: 1, b: 2, c: 3 })
|
|
115
|
+
let numThrows = 0
|
|
88
116
|
function Parent() {
|
|
89
117
|
const ids = useValue('ids', () => Object.keys(theAtom.get()), [])
|
|
90
118
|
return (
|
|
@@ -99,7 +127,10 @@ test("useValue doesn't throw when used in a zombie-child component", async () =>
|
|
|
99
127
|
const value = useValue(
|
|
100
128
|
'value',
|
|
101
129
|
() => {
|
|
102
|
-
if (!(id in theAtom.get()))
|
|
130
|
+
if (!(id in theAtom.get())) {
|
|
131
|
+
numThrows++
|
|
132
|
+
throw new Error('id not found!')
|
|
133
|
+
}
|
|
103
134
|
return theAtom.get()[id]
|
|
104
135
|
},
|
|
105
136
|
[id]
|
|
@@ -108,16 +139,71 @@ test("useValue doesn't throw when used in a zombie-child component", async () =>
|
|
|
108
139
|
}
|
|
109
140
|
|
|
110
141
|
let view: RenderResult
|
|
111
|
-
|
|
142
|
+
act(() => {
|
|
112
143
|
view = render(<Parent />)
|
|
113
144
|
})
|
|
114
145
|
|
|
115
146
|
expect(view!.asFragment().textContent).toMatchInlineSnapshot('"123"')
|
|
116
147
|
|
|
148
|
+
expect(numThrows).toBe(0)
|
|
117
149
|
// remove id 'b' creating a zombie-child
|
|
118
|
-
|
|
150
|
+
act(() => {
|
|
119
151
|
theAtom?.update(({ b: _, ...rest }) => rest)
|
|
120
152
|
})
|
|
121
153
|
|
|
122
154
|
expect(view!.asFragment().textContent).toMatchInlineSnapshot('"13"')
|
|
155
|
+
|
|
156
|
+
expect(numThrows).toBe(1)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test('useValue throws synchronously during render when the computed throws', async () => {
|
|
160
|
+
const theAtom = atom<Error | null>('map', null)
|
|
161
|
+
let caughtError = null as null | Error
|
|
162
|
+
|
|
163
|
+
// Suppress React's console.error for this test
|
|
164
|
+
|
|
165
|
+
function Component({ id }: { id: string }) {
|
|
166
|
+
const value = useValue(
|
|
167
|
+
'value',
|
|
168
|
+
() => {
|
|
169
|
+
const error = theAtom.get()
|
|
170
|
+
if (error) throw error
|
|
171
|
+
return 1
|
|
172
|
+
},
|
|
173
|
+
[id]
|
|
174
|
+
)
|
|
175
|
+
return <>{value}</>
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let view: RenderResult
|
|
179
|
+
act(() => {
|
|
180
|
+
view = render(
|
|
181
|
+
<TestErrorBoundary
|
|
182
|
+
onError={(error) => {
|
|
183
|
+
caughtError = error
|
|
184
|
+
}}
|
|
185
|
+
>
|
|
186
|
+
<Component id="a" />
|
|
187
|
+
</TestErrorBoundary>
|
|
188
|
+
)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
expect(view!.asFragment().textContent).toMatchInlineSnapshot('"1"')
|
|
192
|
+
|
|
193
|
+
// ignore console.error here because react will log the error to console.error
|
|
194
|
+
// even though it's caught by the error boundary
|
|
195
|
+
const originalError = console.error
|
|
196
|
+
console.error = vi.fn()
|
|
197
|
+
try {
|
|
198
|
+
act(() => {
|
|
199
|
+
theAtom.set(new Error('test'))
|
|
200
|
+
})
|
|
201
|
+
} finally {
|
|
202
|
+
console.error = originalError
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
expect(caughtError).toBeInstanceOf(Error)
|
|
206
|
+
expect(caughtError?.message).toBe('test')
|
|
207
|
+
expect(view!.getByTestId('error-boundary')).toBeTruthy()
|
|
208
|
+
expect(view!.getByTestId('error-boundary').textContent).toBe('Error: test')
|
|
123
209
|
})
|
package/src/lib/useValue.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* eslint-disable prefer-rest-params */
|
|
2
2
|
import { Signal, computed, react } from '@tldraw/state'
|
|
3
|
-
import { useMemo,
|
|
3
|
+
import { useMemo, useSyncExternalStore } from 'react'
|
|
4
4
|
|
|
5
5
|
/** @public */
|
|
6
6
|
export function useValue<Value>(value: Signal<Value>): Value
|
|
@@ -49,52 +49,27 @@ export function useValue() {
|
|
|
49
49
|
const deps = args.length === 3 ? args[2] : [args[0]]
|
|
50
50
|
const name = args.length === 3 ? args[0] : `useValue(${args[0].name})`
|
|
51
51
|
|
|
52
|
-
const
|
|
53
|
-
|
|
52
|
+
const { $val, subscribe, getSnapshot } = useMemo(() => {
|
|
53
|
+
const $val =
|
|
54
|
+
args.length === 1 ? (args[0] as Signal<any>) : (computed(name, args[1]) as Signal<any>)
|
|
54
55
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
return {
|
|
57
|
+
$val,
|
|
58
|
+
subscribe: (notify: () => void) => {
|
|
59
|
+
return react(`useValue(${name})`, () => {
|
|
60
|
+
try {
|
|
61
|
+
$val.get()
|
|
62
|
+
} catch {
|
|
63
|
+
// Will be rethrown during render if the component doesn't unmount first.
|
|
64
|
+
}
|
|
65
|
+
notify()
|
|
66
|
+
})
|
|
67
|
+
},
|
|
68
|
+
getSnapshot: () => $val.lastChangedEpoch,
|
|
58
69
|
}
|
|
59
|
-
return computed(name, () => {
|
|
60
|
-
if (isInRender.current) {
|
|
61
|
-
return args[1]()
|
|
62
|
-
} else {
|
|
63
|
-
try {
|
|
64
|
-
return args[1]()
|
|
65
|
-
} catch {
|
|
66
|
-
// when getSnapshot is called outside of the render phase &
|
|
67
|
-
// subsequently throws an error, it might be because we're
|
|
68
|
-
// in a zombie-child state. in that case, we suppress the
|
|
69
|
-
// error and instead return a new dummy value to trigger a
|
|
70
|
-
// react re-render. if we were in a zombie child, react will
|
|
71
|
-
// unmount us instead of re-rendering so the error is
|
|
72
|
-
// irrelevant. if we're not in a zombie-child, react will
|
|
73
|
-
// call `getSnapshot` again in the render phase, and the
|
|
74
|
-
// error will be thrown as expected.
|
|
75
|
-
return {}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
})
|
|
79
70
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
80
71
|
}, deps)
|
|
81
72
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return {
|
|
85
|
-
subscribe: (listen: () => void) => {
|
|
86
|
-
return react(`useValue(${name})`, () => {
|
|
87
|
-
$val.get()
|
|
88
|
-
listen()
|
|
89
|
-
})
|
|
90
|
-
},
|
|
91
|
-
getSnapshot: () => $val.get(),
|
|
92
|
-
}
|
|
93
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
94
|
-
}, [$val])
|
|
95
|
-
|
|
96
|
-
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
|
|
97
|
-
} finally {
|
|
98
|
-
isInRender.current = false
|
|
99
|
-
}
|
|
73
|
+
useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
|
|
74
|
+
return $val.__unsafe__getWithoutCapture()
|
|
100
75
|
}
|