tldraw @tldraw/state-react 信号系统 React 绑定完全指南:track 高阶组件与七大 Hook 的 API 解析与实践

tldraw @tldraw/state-react 信号系统 React 绑定完全指南:track 高阶组件与七大 Hook 的 API 解析与实践 tldraw tldraw/state-react 信号系统 React 绑定完全指南track 高阶组件与七大 Hook 的 API 解析与实践【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. Worlds best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldraw导读tldraw/state-react是 tldraw 信号signals状态库的 React 绑定层它在 packages/state 提供的Atom、Computed、Signal等响应式原语之上提供了一套面向 React 开发者的一等公民 API——包括自动依赖追踪的高阶组件track以及useValue、useAtom、useComputed、useReactor、useQuickReactor、useStateTracking七个 Hook。通过阅读本文你将掌握这些 API 的完整签名与行为语义、它们各自的适用场景与性能取舍、底层基于useSyncExternalStore与EffectScheduler的实现原理并能用信号驱动的方式写出细粒度更新、无多余重渲染的 React 组件——这也是 tldraw 编辑器自身状态管理所采用的方案。本文以该包公开 API 报告 packages/state-react/api-report.api.md 为骨架结合 packages/state-react/DOCS.md、packages/state-react/src 下的源码实现与测试用例展开所有结论均可回到仓库中对应文件验证。一、包概览信号状态如何接入 React1.1 包定位与依赖关系从 packages/state-react/package.json 可以看到该包名为tldraw/state-react版本 5.4.0描述为 tldraw infinite canvas SDK (react bindings for state)即 tldraw 无限画布 SDK 中针对tldraw/state的 React 绑定。其依赖非常克制tldraw/stateworkspace 内联依赖提供Atom、Computed、Signal、EffectScheduler、transact等核心原语tldraw/utilsworkspace 内联依赖提供throttleToNextFrame等工具函数react/react-dom作为 peerDependencies声明兼容^18.2.0 || ^19.2.1。这意味着本包不提供独立的状态存储而是将tldraw/state的响应式能力翻译成 React 可感知的订阅与重渲染机制。1.2 公开 API 全貌根据 packages/state-react/api-report.api.md由 API Extractor 自动生成标注 Do not edit this file该包的完整公开面由 1 个高阶组件与 7 个 Hook 组成名称类型核心作用track高阶组件包装组件自动追踪渲染期间读取的信号并订阅变化useValueHook读取Signal的值并订阅或由函数派生计算值useAtomHook创建组件局部作用域的Atom仅创建一次useComputedHook创建组件局部作用域的Computed自动依赖追踪、带 memo 化useReactorHook响应信号变化运行副作用按动画帧节流useQuickReactorHook响应信号变化立即同步运行副作用不节流useStateTrackingHook手动把一段渲染逻辑包进响应式追踪上下文所有导出集中在 packages/state-react/src/index.ts并在模块加载时通过registerTldrawLibraryVersion上报库版本信息。1.3 最小可运行示例按 packages/state-react/DOCS.md 的安装方式先安装依赖npm install tldraw/state-react tldraw/state react一个最简单的计数器组件同时用到了track与useAtomimport { useAtom, track } from tldraw/state-react const Counter track(function Counter() { const count useAtom(count, 0) return ( button onClick{() count.set(count.get() 1)} Count: {count.get()} /button ) })track会捕获组件渲染期间对count这个 Atom 的读取当count.set(...)触发信号变化时仅该组件重渲染。下面各节将逐一对齐 API 报告中的每个签名展开讲解。二、useValue读取信号值并订阅变化useValue是 API 报告中最基础的读取 Hook存在两个公开重载export function useValueValue(value: SignalValue): Value; export function useValueValue(name: string, fn: () Value, deps: unknown[]): Value;2.1 重载一直接订阅一个 Signalimport { atom } from tldraw/state import { useValue } from tldraw/state-react const name atom(name, World) function Greeter() { const currentName useValue(name) return h1Hello, {currentName}!/h1 }当name变化时Greeter自动重渲染。注意这里可以直接传入Atom或Computed因为二者都实现了Signal接口。2.2 重载二函数派生计算值const firstName atom(firstName, John) const lastName atom(lastName, Doe) function UserProfile() { const fullName useValue(fullName, () { return ${firstName.get()} ${lastName.get()} }, [firstName, lastName]) return divUser: {fullName}/div }该重载内部会先调用computed(name, fn)创建一个Computeddeps 数组与useMemo语义一致——deps 变化时才重建计算对象。2.3 源码级原理useSyncExternalStore 订阅闭包看 packages/state-react/src/lib/useValue.ts 的实现二者共用同一个实现函数export function useValue() { const args arguments const deps args.length 3 ? args[2] : [args[0]] const name args.length 3 ? args[0] : useValue(${args[0].name}) const { $val, subscribe, getSnapshot } useMemo(() { const $val args.length 1 ? (args[0] as Signalany) : (computed(name, args[1]) as Signalany) return { $val, subscribe: (notify: () void) { return react(useValue(${name}), () { try { $val.get() } catch { // Will be rethrown during render if the component doesnt unmount first. } notify() }) }, getSnapshot: () $val.lastChangedEpoch, } }, deps) useSyncExternalStore(subscribe, getSnapshot, getSnapshot) return $val.__unsafe__getWithoutCapture() }几个值得注意的实现细节订阅subscribe内部用tldraw/state的react()建立反应器读取$val.get()以注册依赖然后调用notify()触发 React 重渲染快照getSnapshot返回$val.lastChangedEpoch信号最后变更的纪元号以此驱动useSyncExternalStore判定是否需要重渲染而不是直接比较值返回值通过__unsafe__getWithoutCapture()读取当前值而不额外捕获依赖依赖已在上面的react中注册避免重复追踪异常处理订阅回调中的catch吞掉错误留待渲染阶段再抛出避免订阅阶段抛错导致组件无法卸载。2.4 测试佐证packages/state-react/src/lib/useValue.test.tsx 覆盖了三个重载场景从Computed读取theComputed?.name为useComputed(a1)、从Atom读取、从计算函数读取并验证了useValue在zombie-child父组件先删除数据、子组件尚未卸载场景下不抛错以及计算函数抛错时同步传给错误边界。这些测试给出了该 Hook 的边界行为契约。三、useAtom组件局部的响应式原子状态API 签名见 packages/state-react/api-report.api.mdexport function useAtomValue, Diff unknown( name: string, valueOrInitialiser: (() Value) | Value, options?: AtomOptionsValue, Diff ): AtomValue, Diff;3.1 基本用法与惰性初始化import { useAtom } from tldraw/state-react function TodoItem() { const completed useAtom(completed, false) const text useAtom(text, New todo) // ... }第二个参数可以是值也可以是函数。传入函数时即为惰性初始化——该函数只在组件挂载时执行一次适合昂贵计算function DataProcessor() { const expensiveData useAtom(data, () { return processLargeDataset() // 仅在挂载时运行一次 }) return divProcessing {expensiveData.get().length} items/div }3.2 options 参数AtomOptionsAtomOptionsValue, Diff来自tldraw/state用于定制 Atom 行为const user useAtom( user, { id: 1, name: Alice }, { isEqual: (a, b) a.id b.id, // 仅当 ID 变化时才视为值变更 } )从实现看packages/state-react/src/lib/useAtom.ts该 Hook 本质上是把atom()工厂包进useState的一次性初始化return useState(() { const initialValue typeof valueOrInitialiser function ? (valueOrInitialiser as any)() : valueOrInitialiser return atom(useAtom(${name}), initialValue, options) })[0]注意两点只创建一次即便父组件重渲染Atom 实例也不会重建useState惰性初始化的特性名称自动加前缀调试名会被规范化为useAtom(count)与useValue的useValue(name)、useComputed的useComputed(name)风格一致便于在性能分析器中识别来源。3.3 使用建议useAtom适合组件局部、需要被多个读取方如track组件、useValue、useReactor共享且希望响应式联动的状态。若只是普通 React 本地状态useState仍然够用但一旦你希望该状态被组件外或兄弟组件以信号方式读写Atom 就是正确的载体。四、useComputed组件局部的派生信号API 报告给出两个重载export function useComputedValue(name: string, compute: () Value, deps: any[]): ComputedValue; export function useComputedValue, Diff unknown( name: string, compute: () Value, opts: ComputedOptionsValue, Diff, deps: any[] ): ComputedValue;4.1 基本用法import { useAtom, useComputed } from tldraw/state-react function ShoppingCart() { const items useAtom(items, []) const total useComputed(total, () { return items.get().reduce((sum, item) sum item.price, 0) }, [items]) return divTotal: ${total.get().toFixed(2)}/div }compute函数中通过.get()访问的任意信号都会成为其依赖依赖值变化时Computed才重新计算否则直接复用缓存结果。4.2 带选项的进阶重载ComputedOptionsValue, Diff支持自定义相等比较、diff 计算与历史记录长度const sortedItems useComputed( sortedItems, () items.get().sort((a, b) a.name.localeCompare(b.name)), { isEqual: (a, b) a.length b.length a.every((item, i) item.id b[i].id), // computeDiff: 计算新旧值之间的 diff供历史记录使用 // historyLength: 历史缓冲中保留的 diff 数量上限用于时间旅行/撤销 }, [items] )4.3 源码实现packages/state-react/src/lib/useComputed.ts 的实现非常简洁——它区分重载后把computed()工厂包进useMemoexport function useComputed() { const name arguments[0] const compute arguments[1] const opts arguments.length 3 ? undefined : arguments[2] const deps arguments.length 3 ? arguments[2] : arguments[3] return useMemo(() computed(useComputed(${name}), compute, opts), deps) }由此可以得到与useAtom一致的承诺Computed 实例仅在 deps 变化时重建而计算本身是惰性且带缓存的。当某个信号依赖频繁变化、但派生结果等价isEqual判定相等时下游订阅者不会被通知这是避免无效重渲染的关键机制。五、track让组件自动订阅信号API 签名export function trackT extends FunctionComponentany( baseComponent: T ): React_2.NamedExoticComponentReact_2.ComponentPropsT;5.1 行为语义track返回一个被追踪 被 memo 化的组件自动追踪渲染期间通过.get()读取的任何信号都会被记录为依赖任一依赖变化即触发重渲染自动 memo同时包装进React.memo只有 props 变化或被追踪的信号变化时才重渲染双重条件取并集达到最优渲染频率见 packages/state-react/src/lib/track.ts 的 JSDoc 说明。5.2 与普通组件、memo、forwardRef 的协同track的实现专门处理了 React 特殊组件类型packages/state-react/src/lib/track.tslet compare null const $$typeof baseComponent[$$typeof as keyof typeof baseComponent] if ($$typeof ReactMemoSymbol) { baseComponent (baseComponent as any).type compare (baseComponent as any).compare } if ($$typeof ReactForwardRefSymbol) { return memo(forwardRef(new Proxy((baseComponent as any).render, ProxyHandlers) as any)) as any } return memo(new Proxy(baseComponent, ProxyHandlers) as any, compare) as any对已是React.memo的组件重复 track 是安全的它会解包原 memo 并保留自定义compare函数对React.forwardRef组件对其render函数做代理后重新包装ref 行为保持不变核心机制是ProxyHandlers.apply陷阱track.ts当 React 调用组件函数时把执行包进useStateTracking从而在函数执行期间开启信号捕获。5.3 测试验证的行为契约packages/state-react/src/lib/track.test.tsx 提供了非常清晰的行为证据相同 props 重渲染不触发重渲染memo 生效对已 memo 组件再 track 不会破坏 memo 语义track 组件可使用 ref组件读取的 Atom 变化时自动重渲染a.set(2)后 DOM 从1变为2在useEffect里读取信号不会触发重渲染——只有渲染阶段读取才会被追踪追踪组件同样具备zombie-child 不抛错的容错。5.4 何时用track而非useValue官方文档给出的判据是如果组件已经被track包装则无需再对内部信号使用useValue因为track会自动订阅渲染期间所有.get()访问。useValue的适用场景是未包装组件或只想局部订阅时手动建立订阅。实践中 tldraw 自身大量采用track包装 直接.get()的组合。六、useReactor与useQuickReactor副作用响应API 报告export function useReactor(name: string, reactFn: () void, deps?: any[] | undefined): void; export function useQuickReactor(name: string, reactFn: () void, deps?: any[]): void;6.1useReactor按动画帧节流的副作用import { useReactor } from tldraw/state-react function CanvasRenderer() { const shapes useAtom(shapes, []) useReactor(canvas-update, () { redrawCanvas(shapes.get()) // 每帧至多执行一次 }, [shapes]) return canvas / }实现要点packages/state-react/src/lib/useReactor.tsconst scheduler new EffectScheduler(name, reactFn, { scheduleEffect: (cb) { cancelFn throttleToNextFrame(cb) }, }) scheduler.attach() scheduler.execute() return () { scheduler.detach() cancelFn?.() }用tldraw/utils的throttleToNextFrame把调度节流到下一动画帧挂载时立即执行一次之后信号变化按帧批量触发卸载时detach()并取消未执行的帧回调。6.2useQuickReactor同步立即执行useQuickReactor(sync-data, () { const data criticalData.get() if (data) sendToServer(data) // 立即发送不等下一帧 }, [criticalData])实现packages/state-react/src/lib/useQuickReactor.ts直接使用默认EffectScheduler不注入节流函数因此每次信号变化都同步执行。注意其 deps 默认值为EMPTY_ARRAY。6.3 选择判据packages/state-react/DOCS.md 第 4 节给出了明确的使用分界用useReactor节流视觉更新、动画、DOM 操作、Canvas 渲染、UI 状态同步——这些允许一帧内合并多次变更用useQuickReactor立即数据同步、网络请求、关键状态落库、事件日志——这些要求对每次变化即时响应。一个常见组合同一数据源上用节流 reactor 做视觉反馈、用快速 reactor 做持久化两者互不干扰。七、useStateTracking手动划定响应式区域API 签名export function useStateTrackingT(name: string, render: () T, deps?: unknown[]): T;当不需要整组件追踪、只想让渲染函数的某一段响应信号时用它手动包裹packages/state-react/DOCS.md 第 3.3 节import { useStateTracking } from tldraw/state-react function CustomComponent() { const [regularState, setRegularState] useState(0) const reactiveContent useStateTracking(reactive-section, () { return divCurrent theme: {theme.get()}/div // 仅这段响应信号 }, []) return ( div button onClick{() setRegularState(s s 1)} Regular state: {regularState} /button {reactiveContent} /div ) }7.1 实现原理EffectScheduler useSyncExternalStorepackages/state-react/src/lib/useStateTracking.ts 展示了track底层的完整机制用 ref 持有最新render函数避免每次渲染重建调度器创建EffectScheduler其scheduleEffect回调触发scheduleUpdate即useSyncExternalStore的 notifygetSnapshot返回scheduler.scheduleCount调度次数计数驱动重渲染判定依赖捕获发生在scheduler.execute()时在useEffect中attach()并maybeScheduleEffect()卸载时detach()——注释明确说明这样设计是为了避免在 React 渲染阶段之外执行渲染逻辑并防止zombie组件在卸载前用已删除的数据渲染。因为track的ProxyHandlers.apply正是调用useStateTracking(displayName, () Component.apply(...))见 track.ts所以本 Hook 可视为track的手动模式适合只在局部启用细粒度响应、其余部分保持普通 React 渲染的场景。八、进阶实践性能优化、外部系统集成与调试8.1 用transact合并多次更新来自tldraw/state的事务transaction可以把多个信号写入合并为一次通知下游只重渲染一次import { transact } from tldraw/state function updateUser(userData: UserData) { transact(() { firstName.set(userData.firstName) lastName.set(userData.lastName) email.set(userData.email) }) // 所有订阅方只收到一次变更通知 }8.2 与外部系统集成用useQuickReactor把信号同步到localStorage、WebSocket 等外部通道packages/state-react/DOCS.md 第 5.2 节function LocalStorageSync() { const preferences useAtom(preferences, {}) useQuickReactor(save-preferences, () { localStorage.setItem(prefs, JSON.stringify(preferences.get())) }, [preferences]) // ... }8.3 自定义 Hook 组合七个 Hook 可以自由组合成业务级自定义 Hook例如封装一个计数器逻辑后配合track消费DOCS.md 第 5.3 节。8.4 调试whyAmIRunningtldraw/state提供的whyAmIRunning()可在开发环境打印组件重渲染的依赖链路输出类似TrackedComponent is executing because: ↳ Computed(user.status) changed ↳ Atom(currentUser) changed它同样适用于useReactor等副作用内部帮助定位是谁触发了这次更新。注意仅在process.env.NODE_ENV development下启用避免生产开销。8.5 命名规范信号名称只用于调试与性能分析不需要全局唯一但建议遵循上下文清晰的命名习惯currentUser、selectedShapes、editorMode优于data、state、items——这些名称会以useAtom(currentUser)、useValue(currentUser)的形式出现在调度器与 DevTools 中。另外track包装的组件在 React DevTools 中显示为Memo(ComponentName)props 变化与信号变化被分开追踪可使用 DevTools Profiler 定位瓶颈。九、API 报告中的签名速查对照表以下为 packages/state-react/api-report.api.md 中全部公开签名的汇总便于速查// public export function trackT extends FunctionComponentany(baseComponent: T): React.NamedExoticComponentReact.ComponentPropsT; // public export function useAtomValue, Diff unknown( name: string, valueOrInitialiser: (() Value) | Value, options?: AtomOptionsValue, Diff ): AtomValue, Diff; // public export function useComputedValue(name: string, compute: () Value, deps: any[]): ComputedValue; // public export function useComputedValue, Diff unknown( name: string, compute: () Value, opts: ComputedOptionsValue, Diff, deps: any[] ): ComputedValue; // public export function useQuickReactor(name: string, reactFn: () void, deps?: any[]): void; // public export function useReactor(name: string, reactFn: () void, deps?: any[] | undefined): void; // public export function useStateTrackingT(name: string, render: () T, deps?: unknown[]): T; // public export function useValueValue(value: SignalValue): Value; // public export function useValueValue(name: string, fn: () Value, deps: unknown[]): Value;关键设计约束小结只创建一次是共性承诺useAtom与useComputed都保证信号实例的生命周期绑定组件实例或 deps不会因父组件重渲染而重建渲染期读取才被追踪track只捕获渲染阶段含useStateTracking包裹段的.get()在useEffect等副作用中读取不会引起组件重渲染有测试佐证节流 vs 立即useReactor按帧节流适合视觉更新useQuickReactor同步执行适合关键路径订阅基于useSyncExternalStore快照取lastChangedEpoch/scheduleCount这类单调计数而非对象引用保证并发渲染下的安全订阅。十、进一步探索公开 API 报告含类型签名与 release tagpackages/state-react/api-report.api.md官方配套文档与全部示例packages/state-react/DOCS.md全部源码实现packages/state-react/src/index.ts 及各src/lib/*.ts行为契约测试packages/state-react/src/lib/track.test.tsx、packages/state-react/src/lib/useValue.test.tsx 等底层信号原语Atom、Computed、EffectScheduler、transact、历史记录packages/state/DOCS.mdtldraw/state-react的设计目标是让响应式信号与 React 渲染模型无缝咬合既保留tldraw/state细粒度、可推导的依赖图又借助 React 18 的useSyncExternalStore做到并发安全。理解track与七个 Hook 各自的行为边界后你完全可以在自己的 React 应用中复刻 tldraw 编辑器级别的渲染性能优化策略。【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. Worlds best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldraw创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考