apps/docs/content/sdk-features/side-effects.mdx
Side effects are lifecycle hooks that run when records are created, updated, or deleted. You can use them to intercept and modify records, validate changes, or react to completed operations by updating related data.
The editor uses side effects internally to keep data consistent. When you delete a shape, the editor automatically removes its bindings. When a binding changes, its BindingUtil gets a chance to update the connected shapes. These hooks let independent parts of the system stay in sync without being directly coupled. Side effects live on the store and are exposed as Editor#sideEffects, a StoreSideEffects instance.
Side effects provide six handler types organized around three operations: create, change, and delete. Each operation has a "before" and "after" phase. Handlers are registered per record typeName ('shape', 'binding', 'page', or a custom type); check shape.type inside the handler if you only care about one kind of shape.
| Handler | Runs | Return value |
|---|---|---|
beforeCreate | Before a record is stored | The record to store. Return a modified copy to change it. |
beforeChange | Before an update is stored | The record to store. Return prev to block the change, or a modified next. |
beforeDelete | Before a record is removed | false to prevent deletion. |
afterCreate | After a record is stored | Nothing. Update other records in response. |
afterChange | After an update is stored | Nothing. Update other records in response. |
afterDelete | After a record is removed | Nothing. Clean up references or cascade deletions. |
Use before handlers to modify the record being operated on, and after handlers to update other records in response. Returning prev from beforeChange blocks the change because the store sees no difference from what's already stored and skips the write.
Every handler receives a source parameter indicating whether the change came from user interaction ('user') or remote synchronization ('remote'). This lets you handle local and synced changes differently:
editor.sideEffects.registerAfterCreateHandler('shape', (shape, source) => {
if (source === 'user') {
logUserAction('created shape', shape.type)
}
})
You might auto-save only after user operations, or skip validation for trusted remote data.
Register side effects using the type-specific methods on editor.sideEffects. Each method returns a cleanup function you can call to remove the handler:
const cleanup = editor.sideEffects.registerAfterCreateHandler('shape', (shape, source) => {
if (shape.type === 'note') {
editor.updateShape({ id: shape.id, type: 'note', meta: { createdAt: Date.now() } })
}
})
// Later, when no longer needed
cleanup()
To register several handlers at once, pass an object keyed by type name to editor.sideEffects.register({ shape: { afterCreate, beforeDelete } }); it returns a single cleanup function.
Handlers execute in registration order. If one handler reads a value another handler writes, register the writer first.
Before handlers run inline as each record is written. After handlers are queued and run when the outermost store operation completes, so changes made inside them belong to the same transaction and the same undo step. If an after handler makes further changes, their handlers run in a follow-up pass. The operationComplete handler runs once at the end, after every pass has finished.
Before handlers can enforce constraints on records. This example blocks moves into negative coordinates by returning the previous record:
editor.sideEffects.registerBeforeChangeHandler('shape', (prev, next, source) => {
if (next.x < 0 || next.y < 0) {
return prev // Block the change by returning the previous record
}
return next
})
You can cascade deletions to related records. This example deletes a frame when its last child is removed:
editor.sideEffects.registerAfterDeleteHandler('shape', (shape, source) => {
const parent = editor.getShape(shape.parentId)
if (parent && parent.type === 'frame') {
const siblings = editor.getSortedChildIdsForParent(parent.id)
if (siblings.length === 0) {
editor.deleteShape(parent.id)
}
}
})
The operationComplete handler runs once after all changes in a transaction finish. Use it for expensive operations that should happen once per batch rather than on every record change:
editor.sideEffects.registerOperationCompleteHandler((source) => {
if (source === 'user') {
scheduleAutosave()
}
})