@tldraw/state-react 3.16.0-internal.51e99e128bd4 → 3.16.0-internal.a478398270c6
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 +13 -8
- package/dist-cjs/lib/useReactor.js.map +2 -2
- package/dist-cjs/lib/useValue.js +33 -17
- package/dist-cjs/lib/useValue.js.map +3 -3
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/useReactor.mjs +14 -9
- package/dist-esm/lib/useReactor.mjs.map +2 -2
- package/dist-esm/lib/useValue.mjs +34 -18
- package/dist-esm/lib/useValue.mjs.map +3 -3
- package/package.json +20 -12
- package/src/lib/useComputed.test.tsx +1 -2
- package/src/lib/useReactor.ts +17 -11
- package/src/lib/useValue.test.tsx +4 -90
- package/src/lib/useValue.ts +44 -19
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.a478398270c6",
|
|
41
41
|
"cjs"
|
|
42
42
|
);
|
|
43
43
|
//# sourceMappingURL=index.js.map
|
|
@@ -22,22 +22,27 @@ __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");
|
|
26
25
|
var import_react = require("react");
|
|
27
26
|
function useReactor(name, reactFn, deps = []) {
|
|
28
|
-
(0, import_react.
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
const raf = (0, import_react.useRef)(-1);
|
|
28
|
+
const scheduler = (0, import_react.useMemo)(
|
|
29
|
+
() => new import_state.EffectScheduler(name, reactFn, {
|
|
31
30
|
scheduleEffect: (cb) => {
|
|
32
|
-
|
|
31
|
+
const rafId = requestAnimationFrame(cb);
|
|
32
|
+
raf.current = rafId;
|
|
33
|
+
return rafId;
|
|
33
34
|
}
|
|
34
|
-
})
|
|
35
|
+
}),
|
|
36
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
37
|
+
deps
|
|
38
|
+
);
|
|
39
|
+
(0, import_react.useEffect)(() => {
|
|
35
40
|
scheduler.attach();
|
|
36
41
|
scheduler.execute();
|
|
37
42
|
return () => {
|
|
38
43
|
scheduler.detach();
|
|
39
|
-
|
|
44
|
+
cancelAnimationFrame(raf.current);
|
|
40
45
|
};
|
|
41
|
-
},
|
|
46
|
+
}, [scheduler]);
|
|
42
47
|
}
|
|
43
48
|
//# 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 { useEffect, useMemo, useRef } from 'react'\n\n/** @public */\nexport function useReactor(name: string, reactFn: () => void, deps: undefined | any[] = []) {\n\tconst raf = useRef(-1)\n\tconst scheduler = useMemo(\n\t\t() =>\n\t\t\tnew EffectScheduler(name, reactFn, {\n\t\t\t\tscheduleEffect: (cb) => {\n\t\t\t\t\tconst rafId = requestAnimationFrame(cb)\n\t\t\t\t\traf.current = rafId\n\t\t\t\t\treturn rafId\n\t\t\t\t},\n\t\t\t}),\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t\tdeps\n\t)\n\n\tuseEffect(() => {\n\t\tscheduler.attach()\n\t\tscheduler.execute()\n\t\treturn () => {\n\t\t\tscheduler.detach()\n\t\t\tcancelAnimationFrame(raf.current)\n\t\t}\n\t}, [scheduler])\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAgC;AAChC,mBAA2C;AAGpC,SAAS,WAAW,MAAc,SAAqB,OAA0B,CAAC,GAAG;AAC3F,QAAM,UAAM,qBAAO,EAAE;AACrB,QAAM,gBAAY;AAAA,IACjB,MACC,IAAI,6BAAgB,MAAM,SAAS;AAAA,MAClC,gBAAgB,CAAC,OAAO;AACvB,cAAM,QAAQ,sBAAsB,EAAE;AACtC,YAAI,UAAU;AACd,eAAO;AAAA,MACR;AAAA,IACD,CAAC;AAAA;AAAA,IAEF;AAAA,EACD;AAEA,8BAAU,MAAM;AACf,cAAU,OAAO;AACjB,cAAU,QAAQ;AAClB,WAAO,MAAM;AACZ,gBAAU,OAAO;AACjB,2BAAqB,IAAI,OAAO;AAAA,IACjC;AAAA,EACD,GAAG,CAAC,SAAS,CAAC;AACf;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist-cjs/lib/useValue.js
CHANGED
|
@@ -27,23 +27,39 @@ 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
|
-
|
|
30
|
+
const isInRender = (0, import_react.useRef)(true);
|
|
31
|
+
isInRender.current = true;
|
|
32
|
+
const $val = (0, import_react.useMemo)(() => {
|
|
33
|
+
if (args.length === 1) {
|
|
34
|
+
return args[0];
|
|
35
|
+
}
|
|
36
|
+
return (0, import_state.computed)(name, () => {
|
|
37
|
+
if (isInRender.current) {
|
|
38
|
+
return args[1]();
|
|
39
|
+
} else {
|
|
40
|
+
try {
|
|
41
|
+
return args[1]();
|
|
42
|
+
} catch {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
45
47
|
}, deps);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
try {
|
|
49
|
+
const { subscribe, getSnapshot } = (0, import_react.useMemo)(() => {
|
|
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
|
+
}
|
|
48
64
|
}
|
|
49
65
|
//# 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, 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
|
|
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, useRef, 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 isInRender = useRef(true)\n\tisInRender.current = true\n\n\tconst $val = useMemo(() => {\n\t\tif (args.length === 1) {\n\t\t\treturn args[0]\n\t\t}\n\t\treturn computed(name, () => {\n\t\t\tif (isInRender.current) {\n\t\t\t\treturn args[1]()\n\t\t\t} else {\n\t\t\t\ttry {\n\t\t\t\t\treturn args[1]()\n\t\t\t\t} catch {\n\t\t\t\t\t// when getSnapshot is called outside of the render phase &\n\t\t\t\t\t// subsequently throws an error, it might be because we're\n\t\t\t\t\t// in a zombie-child state. in that case, we suppress the\n\t\t\t\t\t// error and instead return a new dummy value to trigger a\n\t\t\t\t\t// react re-render. if we were in a zombie child, react will\n\t\t\t\t\t// unmount us instead of re-rendering so the error is\n\t\t\t\t\t// irrelevant. if we're not in a zombie-child, react will\n\t\t\t\t\t// call `getSnapshot` again in the render phase, and the\n\t\t\t\t\t// error will be thrown as expected.\n\t\t\t\t\treturn {}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t}, deps)\n\n\ttry {\n\t\tconst { subscribe, getSnapshot } = useMemo(() => {\n\t\t\treturn {\n\t\t\t\tsubscribe: (listen: () => void) => {\n\t\t\t\t\treturn react(`useValue(${name})`, () => {\n\t\t\t\t\t\t$val.get()\n\t\t\t\t\t\tlisten()\n\t\t\t\t\t})\n\t\t\t\t},\n\t\t\t\tgetSnapshot: () => $val.get(),\n\t\t\t}\n\t\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t\t}, [$val])\n\n\t\treturn useSyncExternalStore(subscribe, getSnapshot, getSnapshot)\n\t} finally {\n\t\tisInRender.current = false\n\t}\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,mBAAwC;AACxC,mBAAsD;AA2C/C,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,iBAAa,qBAAO,IAAI;AAC9B,aAAW,UAAU;AAErB,QAAM,WAAO,sBAAQ,MAAM;AAC1B,QAAI,KAAK,WAAW,GAAG;AACtB,aAAO,KAAK,CAAC;AAAA,IACd;AACA,eAAO,uBAAS,MAAM,MAAM;AAC3B,UAAI,WAAW,SAAS;AACvB,eAAO,KAAK,CAAC,EAAE;AAAA,MAChB,OAAO;AACN,YAAI;AACH,iBAAO,KAAK,CAAC,EAAE;AAAA,QAChB,QAAQ;AAUP,iBAAO,CAAC;AAAA,QACT;AAAA,MACD;AAAA,IACD,CAAC;AAAA,EAEF,GAAG,IAAI;AAEP,MAAI;AACH,UAAM,EAAE,WAAW,YAAY,QAAI,sBAAQ,MAAM;AAChD,aAAO;AAAA,QACN,WAAW,CAAC,WAAuB;AAClC,qBAAO,oBAAM,YAAY,IAAI,KAAK,MAAM;AACvC,iBAAK,IAAI;AACT,mBAAO;AAAA,UACR,CAAC;AAAA,QACF;AAAA,QACA,aAAa,MAAM,KAAK,IAAI;AAAA,MAC7B;AAAA,IAED,GAAG,CAAC,IAAI,CAAC;AAET,eAAO,mCAAqB,WAAW,aAAa,WAAW;AAAA,EAChE,UAAE;AACD,eAAW,UAAU;AAAA,EACtB;AACD;",
|
|
6
|
+
"names": []
|
|
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.a478398270c6",
|
|
12
12
|
"esm"
|
|
13
13
|
);
|
|
14
14
|
export {
|
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
import { EffectScheduler } from "@tldraw/state";
|
|
2
|
-
import {
|
|
3
|
-
import { useEffect } from "react";
|
|
2
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
4
3
|
function useReactor(name, reactFn, deps = []) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
const raf = useRef(-1);
|
|
5
|
+
const scheduler = useMemo(
|
|
6
|
+
() => new EffectScheduler(name, reactFn, {
|
|
8
7
|
scheduleEffect: (cb) => {
|
|
9
|
-
|
|
8
|
+
const rafId = requestAnimationFrame(cb);
|
|
9
|
+
raf.current = rafId;
|
|
10
|
+
return rafId;
|
|
10
11
|
}
|
|
11
|
-
})
|
|
12
|
+
}),
|
|
13
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
14
|
+
deps
|
|
15
|
+
);
|
|
16
|
+
useEffect(() => {
|
|
12
17
|
scheduler.attach();
|
|
13
18
|
scheduler.execute();
|
|
14
19
|
return () => {
|
|
15
20
|
scheduler.detach();
|
|
16
|
-
|
|
21
|
+
cancelAnimationFrame(raf.current);
|
|
17
22
|
};
|
|
18
|
-
},
|
|
23
|
+
}, [scheduler]);
|
|
19
24
|
}
|
|
20
25
|
export {
|
|
21
26
|
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 { useEffect, useMemo, useRef } from 'react'\n\n/** @public */\nexport function useReactor(name: string, reactFn: () => void, deps: undefined | any[] = []) {\n\tconst raf = useRef(-1)\n\tconst scheduler = useMemo(\n\t\t() =>\n\t\t\tnew EffectScheduler(name, reactFn, {\n\t\t\t\tscheduleEffect: (cb) => {\n\t\t\t\t\tconst rafId = requestAnimationFrame(cb)\n\t\t\t\t\traf.current = rafId\n\t\t\t\t\treturn rafId\n\t\t\t\t},\n\t\t\t}),\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t\tdeps\n\t)\n\n\tuseEffect(() => {\n\t\tscheduler.attach()\n\t\tscheduler.execute()\n\t\treturn () => {\n\t\t\tscheduler.detach()\n\t\t\tcancelAnimationFrame(raf.current)\n\t\t}\n\t}, [scheduler])\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,uBAAuB;AAChC,SAAS,WAAW,SAAS,cAAc;AAGpC,SAAS,WAAW,MAAc,SAAqB,OAA0B,CAAC,GAAG;AAC3F,QAAM,MAAM,OAAO,EAAE;AACrB,QAAM,YAAY;AAAA,IACjB,MACC,IAAI,gBAAgB,MAAM,SAAS;AAAA,MAClC,gBAAgB,CAAC,OAAO;AACvB,cAAM,QAAQ,sBAAsB,EAAE;AACtC,YAAI,UAAU;AACd,eAAO;AAAA,MACR;AAAA,IACD,CAAC;AAAA;AAAA,IAEF;AAAA,EACD;AAEA,YAAU,MAAM;AACf,cAAU,OAAO;AACjB,cAAU,QAAQ;AAClB,WAAO,MAAM;AACZ,gBAAU,OAAO;AACjB,2BAAqB,IAAI,OAAO;AAAA,IACjC;AAAA,EACD,GAAG,CAAC,SAAS,CAAC;AACf;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,27 +1,43 @@
|
|
|
1
1
|
import { computed, react } from "@tldraw/state";
|
|
2
|
-
import { useMemo, useSyncExternalStore } from "react";
|
|
2
|
+
import { useMemo, useRef, 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
|
-
|
|
7
|
+
const isInRender = useRef(true);
|
|
8
|
+
isInRender.current = true;
|
|
9
|
+
const $val = useMemo(() => {
|
|
10
|
+
if (args.length === 1) {
|
|
11
|
+
return args[0];
|
|
12
|
+
}
|
|
13
|
+
return computed(name, () => {
|
|
14
|
+
if (isInRender.current) {
|
|
15
|
+
return args[1]();
|
|
16
|
+
} else {
|
|
17
|
+
try {
|
|
18
|
+
return args[1]();
|
|
19
|
+
} catch {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
});
|
|
22
24
|
}, deps);
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
try {
|
|
26
|
+
const { subscribe, getSnapshot } = useMemo(() => {
|
|
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
|
+
}
|
|
25
41
|
}
|
|
26
42
|
export {
|
|
27
43
|
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, 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
|
|
5
|
-
"mappings": "AACA,SAAiB,UAAU,aAAa;AACxC,SAAS,SAAS,4BAA4B;
|
|
6
|
-
"names": [
|
|
4
|
+
"sourcesContent": ["/* eslint-disable prefer-rest-params */\nimport { Signal, computed, react } from '@tldraw/state'\nimport { useMemo, useRef, 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 isInRender = useRef(true)\n\tisInRender.current = true\n\n\tconst $val = useMemo(() => {\n\t\tif (args.length === 1) {\n\t\t\treturn args[0]\n\t\t}\n\t\treturn computed(name, () => {\n\t\t\tif (isInRender.current) {\n\t\t\t\treturn args[1]()\n\t\t\t} else {\n\t\t\t\ttry {\n\t\t\t\t\treturn args[1]()\n\t\t\t\t} catch {\n\t\t\t\t\t// when getSnapshot is called outside of the render phase &\n\t\t\t\t\t// subsequently throws an error, it might be because we're\n\t\t\t\t\t// in a zombie-child state. in that case, we suppress the\n\t\t\t\t\t// error and instead return a new dummy value to trigger a\n\t\t\t\t\t// react re-render. if we were in a zombie child, react will\n\t\t\t\t\t// unmount us instead of re-rendering so the error is\n\t\t\t\t\t// irrelevant. if we're not in a zombie-child, react will\n\t\t\t\t\t// call `getSnapshot` again in the render phase, and the\n\t\t\t\t\t// error will be thrown as expected.\n\t\t\t\t\treturn {}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t}, deps)\n\n\ttry {\n\t\tconst { subscribe, getSnapshot } = useMemo(() => {\n\t\t\treturn {\n\t\t\t\tsubscribe: (listen: () => void) => {\n\t\t\t\t\treturn react(`useValue(${name})`, () => {\n\t\t\t\t\t\t$val.get()\n\t\t\t\t\t\tlisten()\n\t\t\t\t\t})\n\t\t\t\t},\n\t\t\t\tgetSnapshot: () => $val.get(),\n\t\t\t}\n\t\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t\t}, [$val])\n\n\t\treturn useSyncExternalStore(subscribe, getSnapshot, getSnapshot)\n\t} finally {\n\t\tisInRender.current = false\n\t}\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAiB,UAAU,aAAa;AACxC,SAAS,SAAS,QAAQ,4BAA4B;AA2C/C,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,aAAa,OAAO,IAAI;AAC9B,aAAW,UAAU;AAErB,QAAM,OAAO,QAAQ,MAAM;AAC1B,QAAI,KAAK,WAAW,GAAG;AACtB,aAAO,KAAK,CAAC;AAAA,IACd;AACA,WAAO,SAAS,MAAM,MAAM;AAC3B,UAAI,WAAW,SAAS;AACvB,eAAO,KAAK,CAAC,EAAE;AAAA,MAChB,OAAO;AACN,YAAI;AACH,iBAAO,KAAK,CAAC,EAAE;AAAA,QAChB,QAAQ;AAUP,iBAAO,CAAC;AAAA,QACT;AAAA,MACD;AAAA,IACD,CAAC;AAAA,EAEF,GAAG,IAAI;AAEP,MAAI;AACH,UAAM,EAAE,WAAW,YAAY,IAAI,QAAQ,MAAM;AAChD,aAAO;AAAA,QACN,WAAW,CAAC,WAAuB;AAClC,iBAAO,MAAM,YAAY,IAAI,KAAK,MAAM;AACvC,iBAAK,IAAI;AACT,mBAAO;AAAA,UACR,CAAC;AAAA,QACF;AAAA,QACA,aAAa,MAAM,KAAK,IAAI;AAAA,MAC7B;AAAA,IAED,GAAG,CAAC,IAAI,CAAC;AAET,WAAO,qBAAqB,WAAW,aAAa,WAAW;AAAA,EAChE,UAAE;AACD,eAAW,UAAU;AAAA,EACtB;AACD;",
|
|
6
|
+
"names": []
|
|
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": "A tiny little drawing app (react bindings for state).",
|
|
4
|
+
"version": "3.16.0-internal.a478398270c6",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "tldraw Inc.",
|
|
7
7
|
"email": "hello@tldraw.com"
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
},
|
|
18
18
|
"keywords": [
|
|
19
19
|
"tldraw",
|
|
20
|
-
"sdk",
|
|
21
20
|
"drawing",
|
|
22
21
|
"app",
|
|
23
22
|
"development",
|
|
@@ -32,29 +31,38 @@
|
|
|
32
31
|
"src"
|
|
33
32
|
],
|
|
34
33
|
"scripts": {
|
|
35
|
-
"test-ci": "
|
|
36
|
-
"test": "yarn run -T
|
|
37
|
-
"test-coverage": "
|
|
34
|
+
"test-ci": "lazy inherit",
|
|
35
|
+
"test": "yarn run -T jest",
|
|
36
|
+
"test-coverage": "lazy inherit",
|
|
38
37
|
"build": "yarn run -T tsx ../../internal/scripts/build-package.ts",
|
|
39
38
|
"build-api": "yarn run -T tsx ../../internal/scripts/build-api.ts",
|
|
40
39
|
"prepack": "yarn run -T tsx ../../internal/scripts/prepack.ts",
|
|
41
40
|
"postpack": "../../internal/scripts/postpack.sh",
|
|
42
41
|
"pack-tarball": "yarn pack",
|
|
43
|
-
"lint": "yarn run -T tsx ../../internal/scripts/lint.ts"
|
|
44
|
-
|
|
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"
|
|
45
53
|
},
|
|
46
54
|
"dependencies": {
|
|
47
|
-
"@tldraw/state": "3.16.0-internal.
|
|
48
|
-
"@tldraw/utils": "3.16.0-internal.
|
|
55
|
+
"@tldraw/state": "3.16.0-internal.a478398270c6",
|
|
56
|
+
"@tldraw/utils": "3.16.0-internal.a478398270c6"
|
|
49
57
|
},
|
|
50
58
|
"devDependencies": {
|
|
59
|
+
"@testing-library/jest-dom": "^5.17.0",
|
|
51
60
|
"@testing-library/react": "^15.0.7",
|
|
52
61
|
"@types/lodash": "^4.17.14",
|
|
53
62
|
"@types/react": "^18.3.18",
|
|
54
63
|
"lodash": "^4.17.21",
|
|
55
64
|
"react": "^18.3.1",
|
|
56
|
-
"react-dom": "^18.3.1"
|
|
57
|
-
"vitest": "^3.2.4"
|
|
65
|
+
"react-dom": "^18.3.1"
|
|
58
66
|
},
|
|
59
67
|
"peerDependencies": {
|
|
60
68
|
"react": "^18.2.0 || ^19.0.0",
|
|
@@ -1,7 +1,6 @@
|
|
|
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'
|
|
5
4
|
import { useAtom } from './useAtom'
|
|
6
5
|
import { useComputed } from './useComputed'
|
|
7
6
|
import { useValue } from './useValue'
|
|
@@ -77,7 +76,7 @@ test('useComputed allows optionally passing options', async () => {
|
|
|
77
76
|
let theComputed = null as null | Computed<number>
|
|
78
77
|
let theAtom = null as null | Atom<number>
|
|
79
78
|
let setCount = null as null | ((count: number) => void)
|
|
80
|
-
const isEqual =
|
|
79
|
+
const isEqual = jest.fn((a, b) => a === b)
|
|
81
80
|
function Component() {
|
|
82
81
|
const [count, _setCount] = useState(0)
|
|
83
82
|
setCount = _setCount
|
package/src/lib/useReactor.ts
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
import { EffectScheduler } from '@tldraw/state'
|
|
2
|
-
import {
|
|
3
|
-
import { useEffect } from 'react'
|
|
2
|
+
import { useEffect, useMemo, useRef } from 'react'
|
|
4
3
|
|
|
5
4
|
/** @public */
|
|
6
5
|
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
|
+
|
|
7
20
|
useEffect(() => {
|
|
8
|
-
let cancelFn: () => void | undefined
|
|
9
|
-
const scheduler = new EffectScheduler(name, reactFn, {
|
|
10
|
-
scheduleEffect: (cb) => {
|
|
11
|
-
cancelFn = throttleToNextFrame(cb)
|
|
12
|
-
},
|
|
13
|
-
})
|
|
14
21
|
scheduler.attach()
|
|
15
22
|
scheduler.execute()
|
|
16
23
|
return () => {
|
|
17
24
|
scheduler.detach()
|
|
18
|
-
|
|
25
|
+
cancelAnimationFrame(raf.current)
|
|
19
26
|
}
|
|
20
|
-
|
|
21
|
-
}, deps)
|
|
27
|
+
}, [scheduler])
|
|
22
28
|
}
|
|
@@ -1,37 +1,10 @@
|
|
|
1
1
|
import { RenderResult, act, render } from '@testing-library/react'
|
|
2
2
|
import { Atom, Computed, atom } from '@tldraw/state'
|
|
3
|
-
import {
|
|
4
|
-
import { vi } from 'vitest'
|
|
3
|
+
import { useState } from 'react'
|
|
5
4
|
import { useAtom } from './useAtom'
|
|
6
5
|
import { useComputed } from './useComputed'
|
|
7
6
|
import { useValue } from './useValue'
|
|
8
7
|
|
|
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
|
-
|
|
35
8
|
test('useValue returns a value from a computed', async () => {
|
|
36
9
|
let theComputed = null as null | Computed<number>
|
|
37
10
|
let theAtom = null as null | Atom<number>
|
|
@@ -112,7 +85,6 @@ test('useValue returns a value from a compute function', async () => {
|
|
|
112
85
|
|
|
113
86
|
test("useValue doesn't throw when used in a zombie-child component", async () => {
|
|
114
87
|
const theAtom = atom<Record<string, number>>('map', { a: 1, b: 2, c: 3 })
|
|
115
|
-
let numThrows = 0
|
|
116
88
|
function Parent() {
|
|
117
89
|
const ids = useValue('ids', () => Object.keys(theAtom.get()), [])
|
|
118
90
|
return (
|
|
@@ -127,10 +99,7 @@ test("useValue doesn't throw when used in a zombie-child component", async () =>
|
|
|
127
99
|
const value = useValue(
|
|
128
100
|
'value',
|
|
129
101
|
() => {
|
|
130
|
-
if (!(id in theAtom.get()))
|
|
131
|
-
numThrows++
|
|
132
|
-
throw new Error('id not found!')
|
|
133
|
-
}
|
|
102
|
+
if (!(id in theAtom.get())) throw new Error('id not found!')
|
|
134
103
|
return theAtom.get()[id]
|
|
135
104
|
},
|
|
136
105
|
[id]
|
|
@@ -139,71 +108,16 @@ test("useValue doesn't throw when used in a zombie-child component", async () =>
|
|
|
139
108
|
}
|
|
140
109
|
|
|
141
110
|
let view: RenderResult
|
|
142
|
-
act(() => {
|
|
111
|
+
await act(() => {
|
|
143
112
|
view = render(<Parent />)
|
|
144
113
|
})
|
|
145
114
|
|
|
146
115
|
expect(view!.asFragment().textContent).toMatchInlineSnapshot('"123"')
|
|
147
116
|
|
|
148
|
-
expect(numThrows).toBe(0)
|
|
149
117
|
// remove id 'b' creating a zombie-child
|
|
150
|
-
act(() => {
|
|
118
|
+
await act(() => {
|
|
151
119
|
theAtom?.update(({ b: _, ...rest }) => rest)
|
|
152
120
|
})
|
|
153
121
|
|
|
154
122
|
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')
|
|
209
123
|
})
|
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, useSyncExternalStore } from 'react'
|
|
3
|
+
import { useMemo, useRef, useSyncExternalStore } from 'react'
|
|
4
4
|
|
|
5
5
|
/** @public */
|
|
6
6
|
export function useValue<Value>(value: Signal<Value>): Value
|
|
@@ -49,27 +49,52 @@ 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
|
-
|
|
54
|
-
args.length === 1 ? (args[0] as Signal<any>) : (computed(name, args[1]) as Signal<any>)
|
|
52
|
+
const isInRender = useRef(true)
|
|
53
|
+
isInRender.current = true
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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,
|
|
55
|
+
const $val = useMemo(() => {
|
|
56
|
+
if (args.length === 1) {
|
|
57
|
+
return args[0]
|
|
69
58
|
}
|
|
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
|
+
})
|
|
70
79
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
71
80
|
}, deps)
|
|
72
81
|
|
|
73
|
-
|
|
74
|
-
|
|
82
|
+
try {
|
|
83
|
+
const { subscribe, getSnapshot } = useMemo(() => {
|
|
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
|
+
}
|
|
75
100
|
}
|