tldraw SDK用 After-Create / After-Change 副作用处理器跨记录维持数据不变量【免费下载链接】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 SDK 构建无限画板应用时经常需要让一份数据的变化自动约束另一份数据例如同一页面上只能有一个红色图形。本文基于官方示例 after-create-update-shape 展开完整讲解editor.sideEffects.registerAfterCreateHandler与registerAfterChangeHandler的语义、配套的可运行示例代码以及底层 StoreSideEffects 的调用链帮你掌握用副作用钩子side effects在记录写入 store 之后更新其他记录的正确姿势。示例要解决的问题页面上只能存在一个红色图形该示例的核心思路只有一句话当某个 shape 被创建或被修改后如果它恰好是红色的就把同一页面上其他所有红色图形改成黑色。这样至多一个红色 shape的不变量始终成立。README 中给出的手动验证方式是选中任意一个黑色文字在样式面板里把它的颜色改成红色原本红色的那个文字会立刻变黑。为什么必须同时注册 create 和 change 两种处理器因为一个红色 shape 可能以两种方式进入画板直接被创建出来时 props 就是color: red触发 after-create先以黑色创建随后被用户或代码改成红色触发 after-change。只监听其中一种路径都会留下漏洞。示例组件 AfterCreateUpdateShapeExample.tsx 中的注册代码如下Tldraw onMount{(editor) { editor.sideEffects.registerAfterCreateHandler(shape, (shape) { ensureOnlyOneRedShape(editor, shape.id) }) editor.sideEffects.registerAfterChangeHandler(shape, (_prevShape, nextShape) { ensureOnlyOneRedShape(editor, nextShape.id) }) createDemoShapes(editor) }} /两个处理器都委托给同一个函数ensureOnlyOneRedShape下面完整拆解它的实现。核心实现ensureOnlyOneRedShape 逐段解析以下是示例中的完整实现节选自 AfterCreateUpdateShapeExample.tsx每一段都对应一个容易踩坑的细节type ShapeWithColor ExtractTLShape, { props: { color: string } } // [1] 如果目标 shape 是红色把同页面上其他红色 shape 全部改黑 function ensureOnlyOneRedShape(editor: Editor, shapeId: TLShapeId) { const shape editor.getShape(shapeId)! if (!isRedShape(shape)) return const pageId editor.getAncestorPageId(shape.id)! const otherRedShapesOnPage Array.from(editor.getPageShapeIds(pageId)) .map((id) editor.getShape(id)!) .filter( (otherShape): otherShape is ShapeWithColor otherShape.id ! shape.id isRedShape(otherShape) ) editor.updateShapes( otherRedShapesOnPage.map( (shape) ({ id: shape.id, type: shape.type, props: { color: black }, }) as TLShapePartial // [2] ) ) } function isRedShape(shape: TLShape) { return color in shape.props shape.props.color red }细节 1先判断红色否则就是死循环if (!isRedShape(shape)) return这一行是整个示例的安全阀也是 README 特别强调的一点after-change handlers also fire for the updates they cause, make sure your handler is a no-op for records that already satisfy your rule, or it will loop.after-change 处理器也会因处理器自己引发的更新而再次触发所以处理器对已经满足规则的记录必须是空操作否则会无限循环。触发链是这样的shape A 变红 → after-change 处理器把同页的红色 shape B 改成黑色 → 对 B 而言这就是一次 change → B 的 after-change 处理器再次执行 → 此时 B 是黑色isRedShape为 false直接 return链条终止。如果这里不提前返回、每次都执行updateShapes就会形成 A→B→A 的无限触发循环。编写任何跨记录修正型处理器时都必须保证规则满足时处理器是幂等的 no-op。细节 2不是每种 shape 都有 color 属性isRedShape中先做color in shape.props检查因为并非所有 shape 类型都带color属性——示例文件底部注释指出比如图片和视频就没有color。在shape: shape这个统一的记录类型上props是各具体 shape 类型的联合读取属性前必须先做存在性判断否则会拿到undefined或触发类型错误。细节 3为什么需要as TLShapePartial断言ShapeWithColor是所有带color属性的 shape 类型的联合。构造{ id, type, props: { color: black } }时type是运行时值TypeScript 无法把这个对象字面量收窄回联合类型的某一个具体成员因此示例用一个断言把结果标记为TLShapePartial。断言并不改变运行数据——id与type都来自原始 shape 本身所以这次更新仍是类型安全的。完整可运行示例将以下代码作为 React 组件即可复现整个演示完整版见 AfterCreateUpdateShapeExample.tsximport { Editor, TLShape, TLShapeId, TLShapePartial, Tldraw, createShapeId, toRichText, } from tldraw import tldraw/tldraw.css type ShapeWithColor ExtractTLShape, { props: { color: string } } function ensureOnlyOneRedShape(editor: Editor, shapeId: TLShapeId) { const shape editor.getShape(shapeId)! if (!isRedShape(shape)) return const pageId editor.getAncestorPageId(shape.id)! const otherRedShapesOnPage Array.from(editor.getPageShapeIds(pageId)) .map((id) editor.getShape(id)!) .filter( (otherShape): otherShape is ShapeWithColor otherShape.id ! shape.id isRedShape(otherShape) ) editor.updateShapes( otherRedShapesOnPage.map( (shape) ({ id: shape.id, type: shape.type, props: { color: black }, }) as TLShapePartial ) ) } function isRedShape(shape: TLShape) { return color in shape.props shape.props.color red } export default function AfterCreateUpdateShapeExample() { return ( div classNametldraw__editor Tldraw onMount{(editor) { editor.sideEffects.registerAfterCreateHandler(shape, (shape) { ensureOnlyOneRedShape(editor, shape.id) }) editor.sideEffects.registerAfterChangeHandler(shape, (_prevShape, nextShape) { ensureOnlyOneRedShape(editor, nextShape.id) }) createDemoShapes(editor) }} / /div ) } function createDemoShapes(editor: Editor) { editor .createShapes( there can only be one red shape.split( ).map((word, i) ({ id: createShapeId(), type: text, y: i * 30, props: { color: i 5 ? red : black, richText: toRichText(word), }, })) ) .zoomToFit({ animation: { duration: 0 } }) }初始化函数createDemoShapes用createShapes一次性创建 7 个文字 shape拼成 there can only be one red shape 这句话其中第 6 个i 5为红色其余为黑色最后zoomToFit让画板自动适配视图。注意createShapes批量创建时每个 shape 写入 store 后都会各自触发 after-create 处理器——这正演示了钩子对程序化创建同样生效。底层原理StoreSideEffects 的处理器生命周期上面的示例建立在editor.sideEffects这一 API 之上。从源码结构看sideEffects是挂在 store 上的 StoreSideEffects 实例其类型定义为节选自 StoreSideEffects.tsexport type StoreAfterCreateHandlerR extends UnknownRecord ( record: R, source: remote | user ) void export type StoreAfterChangeHandlerR extends UnknownRecord ( prev: R, next: R, source: remote | user ) void几个对本示例有直接意义的机制1. 按记录类型注册处理器在注册顺序中依次执行。registerAfterCreateHandler(shape, handler)把处理器存入以typeName为键的数组StoreSideEffects.ts#L509-L517写入记录后store 调用handleAfterCreate遍历同类型的全部处理器StoreSideEffects.ts#L273-L282。这意味着处理器只在目标类型的记录发生变化时被调用而shape是统一的记录类型——具体是文字、矩形还是箭头需要在处理器内部用shape.type进一步区分。2. after 处理器在记录已经写入 store 之后运行。这正是 README 的核心论断run after a record has been written to the store, that makes them the right place to update other records in response to a change在记录写入 store 之后运行这使它们成为响应变化去更新其他记录的合适位置。在 Store.ts 中可以看到 store 在写入前后调用this.sideEffects.handleAfterChange(before, after, source)的实际接线。这也解释了为什么处理器里能安全地调用editor.getShape拿到刚刚写入的 shape——数据已经在 store 里了。3.source参数区分来源。第二个参数source为user本地用户操作或remote远端同步到达。本示例没有用到它但如果你的规则只想约束本地操作、放行受信远端数据可以像这样加一层过滤editor.sideEffects.registerAfterChangeHandler(shape, (_prev, next, source) { if (source ! user) return ensureOnlyOneRedShape(editor, next.id) })4. 注册返回清理函数。两个register*方法都返回一个移除该处理器的回调StoreSideEffects.ts#L516。在 React 中注册时通常保存返回值并在组件卸载时调用避免已卸载组件的闭包继续修改编辑器useEffect(() { const disposeCreate editor.sideEffects.registerAfterCreateHandler(shape, (shape) ensureOnlyOneRedShape(editor, shape.id) ) const disposeChange editor.sideEffects.registerAfterChangeHandler(shape, (_prev, next) ensureOnlyOneRedShape(editor, next.id) ) return () { disposeCreate() disposeChange() } }, [editor])5. after 处理器引发的新变更会进入后续处理轮次。按官方文档 side-effects 的说明before 处理器在每条记录写入时内联执行after 处理器则被排队在外层 store 操作完成后运行因此处理器内做的修改与触发它的变更属于同一事务、同一撤销步如果 after 处理器又引发了新的记录变更那些变更的处理器会在后续轮次follow-up pass中执行——这正是A 变红 → 处理器把 B 改黑 → B 的 after-change 再次触发这条链能够成立、且最终能收敛的机制前提。与其他钩子的分工Side effects 一共有六类处理器before/after × create/change/delete选型原则在源码注释里写得很明确你的目的应使用的处理器修改正在被操作的那条记录本身registerBeforeCreateHandler/registerBeforeChangeHandler可返回修改后的记录beforeChange返回prev可拦截本次更新响应变化去更新其他记录registerAfterCreateHandler/registerAfterChangeHandler返回值为 void阻止删除 / 级联清理registerBeforeDeleteHandler返回false阻止/registerAfterDeleteHandler本示例属于第二种updateShapes的目标是页面上别的shape而非触发者自己所以必须用 after 处理器而非 before 处理器。同目录下的兄弟示例分别演示了另外三种路径可作为对照before-create-update-shape、after-delete-shape、before-delete-shape。小结这篇示例浓缩了用 tldraw side effects 实现跨记录数据不变量的完整套路用registerAfterCreateHandlerregisterAfterChangeHandler同时覆盖创建即为红和后来变红两条路径处理器先做幂等检查isRedShape为 false 直接返回避免自身引发的更新造成循环用getAncestorPageIdgetPageShapeIds圈定作用域只对同页记录做约束读取联合类型 props 前先做color in props存在性检查构造 partial 时用断言配合值来自原记录保证类型安全注册时保留返回的清理函数在组件卸载时移除处理器。配套源码可进一步阅读示例组件 AfterCreateUpdateShapeExample.tsx、钩子实现 StoreSideEffects.ts、store 接线 Store.ts以及官方文档 Side Effects 中对执行顺序与operationComplete的完整说明。【免费下载链接】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),仅供参考
