@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 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.51e99e128bd4",
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.useEffect)(() => {
29
- let cancelFn;
30
- const scheduler = new import_state.EffectScheduler(name, reactFn, {
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
- cancelFn = (0, import_utils.throttleToNextFrame)(cb);
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
- cancelFn?.();
44
+ cancelAnimationFrame(raf.current);
40
45
  };
41
- }, deps);
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 { 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;",
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
  }
@@ -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 { $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
- };
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
- (0, import_react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
47
- return $val.__unsafe__getWithoutCapture();
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 { $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"]
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
  }
@@ -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.51e99e128bd4",
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 { throttleToNextFrame } from "@tldraw/utils";
3
- import { useEffect } from "react";
2
+ import { useEffect, useMemo, useRef } from "react";
4
3
  function useReactor(name, reactFn, deps = []) {
5
- useEffect(() => {
6
- let cancelFn;
7
- const scheduler = new EffectScheduler(name, reactFn, {
4
+ const raf = useRef(-1);
5
+ const scheduler = useMemo(
6
+ () => new EffectScheduler(name, reactFn, {
8
7
  scheduleEffect: (cb) => {
9
- cancelFn = throttleToNextFrame(cb);
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
- cancelFn?.();
21
+ cancelAnimationFrame(raf.current);
17
22
  };
18
- }, deps);
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 { 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;",
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 { $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
- };
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
- useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
24
- return $val.__unsafe__getWithoutCapture();
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 { $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"]
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": "tldraw infinite canvas SDK (react bindings for state).",
4
- "version": "3.16.0-internal.51e99e128bd4",
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": "yarn run -T vitest run --passWithNoTests",
36
- "test": "yarn run -T vitest --passWithNoTests",
37
- "test-coverage": "yarn run -T vitest run --coverage --passWithNoTests",
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
- "context": "yarn run -T tsx ../../internal/scripts/context.ts"
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.51e99e128bd4",
48
- "@tldraw/utils": "3.16.0-internal.51e99e128bd4"
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 = vi.fn((a, b) => a === b)
79
+ const isEqual = jest.fn((a, b) => a === b)
81
80
  function Component() {
82
81
  const [count, _setCount] = useState(0)
83
82
  setCount = _setCount
@@ -1,22 +1,28 @@
1
1
  import { EffectScheduler } from '@tldraw/state'
2
- import { throttleToNextFrame } from '@tldraw/utils'
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
- cancelFn?.()
25
+ cancelAnimationFrame(raf.current)
19
26
  }
20
- // eslint-disable-next-line react-hooks/exhaustive-deps
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 { Component, ReactNode, useState } from 'react'
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
  })
@@ -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 { $val, subscribe, getSnapshot } = useMemo(() => {
53
- const $val =
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
- 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,
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
- useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
74
- return $val.__unsafe__getWithoutCapture()
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
  }