UNPKG

@tldraw/state-react

Version:

tldraw infinite canvas SDK (react bindings for state).

8 lines (7 loc) 3.98 kB
{ "version": 3, "sources": ["../../src/lib/useStateTracking.ts"], "sourcesContent": ["import { EffectScheduler } from '@tldraw/state'\nimport React from 'react'\n\n/**\n * Wraps some synchronous react render logic in a reactive tracking context.\n *\n * This allows you to use reactive values transparently.\n *\n * See the `track` component wrapper, which uses this under the hood.\n *\n * @param name - A debug name for the reactive tracking context\n * @param render - The render function that accesses reactive values\n * @param deps - Optional dependency array to control when the tracking context is recreated\n * @returns The result of calling the render function\n *\n * @example\n * ```ts\n * function MyComponent() {\n * return useStateTracking('MyComponent', () => {\n * const editor = useEditor()\n * return <div>Num shapes: {editor.getCurrentPageShapes().length}</div>\n * })\n * }\n * ```\n *\n *\n * @public\n */\nexport function useStateTracking<T>(name: string, render: () => T, deps: unknown[] = []): T {\n\t// This hook creates an effect scheduler that will trigger re-renders when its reactive dependencies change, but it\n\t// defers the actual execution of the effect to the consumer of this hook.\n\n\t// We need the exec fn to always be up-to-date when calling scheduler.execute() but it'd be wasteful to\n\t// instantiate a new EffectScheduler on every render, so we use an immediately-updated ref\n\t// to wrap it\n\tconst renderRef = React.useRef(render)\n\trenderRef.current = render\n\n\tconst [scheduler, subscribe, getSnapshot] = React.useMemo(() => {\n\t\tlet scheduleUpdate = null as null | (() => void)\n\t\t// useSyncExternalStore requires a subscribe function that returns an unsubscribe function\n\t\tconst subscribe = (cb: () => void) => {\n\t\t\tscheduleUpdate = cb\n\t\t\treturn () => {\n\t\t\t\tscheduleUpdate = null\n\t\t\t}\n\t\t}\n\n\t\tconst scheduler = new EffectScheduler(\n\t\t\t`useStateTracking(${name})`,\n\t\t\t// this is what `scheduler.execute()` will call\n\t\t\t() => renderRef.current?.(),\n\t\t\t// this is what will be invoked when @tldraw/state detects a change in an upstream reactive value\n\t\t\t{\n\t\t\t\tscheduleEffect() {\n\t\t\t\t\tscheduleUpdate?.()\n\t\t\t\t},\n\t\t\t}\n\t\t)\n\n\t\t// we use an incrementing number based on when this\n\t\tconst getSnapshot = () => scheduler.scheduleCount\n\n\t\treturn [scheduler, subscribe, getSnapshot]\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t}, [name, ...deps])\n\n\tReact.useSyncExternalStore(subscribe, getSnapshot, getSnapshot)\n\n\t// reactive dependencies are captured when `scheduler.execute()` is called\n\t// and then to make it reactive we wait for a `useEffect` to 'attach'\n\t// this allows us to avoid rendering outside of React's render phase\n\t// and avoid 'zombie' components that try to render with bad/deleted data before\n\t// react has a chance to umount them.\n\tReact.useEffect(() => {\n\t\tscheduler.attach()\n\t\t// do not execute, we only do that in render\n\t\tscheduler.maybeScheduleEffect()\n\t\treturn () => {\n\t\t\tscheduler.detach()\n\t\t}\n\t}, [scheduler])\n\n\treturn scheduler.execute()\n}\n"], "mappings": "AAAA,SAAS,uBAAuB;AAChC,OAAO,WAAW;AA2BX,SAAS,iBAAoB,MAAc,QAAiB,OAAkB,CAAC,GAAM;AAO3F,QAAM,YAAY,MAAM,OAAO,MAAM;AACrC,YAAU,UAAU;AAEpB,QAAM,CAAC,WAAW,WAAW,WAAW,IAAI,MAAM,QAAQ,MAAM;AAC/D,QAAI,iBAAiB;AAErB,UAAMA,aAAY,CAAC,OAAmB;AACrC,uBAAiB;AACjB,aAAO,MAAM;AACZ,yBAAiB;AAAA,MAClB;AAAA,IACD;AAEA,UAAMC,aAAY,IAAI;AAAA,MACrB,oBAAoB,IAAI;AAAA;AAAA,MAExB,MAAM,UAAU,UAAU;AAAA;AAAA,MAE1B;AAAA,QACC,iBAAiB;AAChB,2BAAiB;AAAA,QAClB;AAAA,MACD;AAAA,IACD;AAGA,UAAMC,eAAc,MAAMD,WAAU;AAEpC,WAAO,CAACA,YAAWD,YAAWE,YAAW;AAAA,EAE1C,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC;AAElB,QAAM,qBAAqB,WAAW,aAAa,WAAW;AAO9D,QAAM,UAAU,MAAM;AACrB,cAAU,OAAO;AAEjB,cAAU,oBAAoB;AAC9B,WAAO,MAAM;AACZ,gBAAU,OAAO;AAAA,IAClB;AAAA,EACD,GAAG,CAAC,SAAS,CAAC;AAEd,SAAO,UAAU,QAAQ;AAC1B;", "names": ["subscribe", "scheduler", "getSnapshot"] }