--- title: Side effects created_at: 12/20/2025 updated_at: 12/29/2025 keywords: - side effects - store - hooks - validation - cleanup status: published date: 12/20/2025 readability: 9 voice: 9 completeness: 9 accuracy: 10 notes: '' --- 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](/sdk-features/store) and are exposed as [Editor#sideEffects](?), a [StoreSideEffects](?) instance. ## How it works ### Before and after handlers 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. ### Source tracking 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: ```typescript 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. ### Registration and cleanup Register side effects using the type-specific methods on `editor.sideEffects`. Each method returns a cleanup function you can call to remove the handler: ```typescript 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. ### Execution order 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. ## Use cases ### Constraining shape positions Before handlers can enforce constraints on records. This example blocks moves into negative coordinates by returning the previous record: ```typescript 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 }) ``` ### Cascading deletions You can cascade deletions to related records. This example deletes a frame when its last child is removed: ```typescript 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) } } }) ``` ### Batch processing with operationComplete 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: ```typescript editor.sideEffects.registerOperationCompleteHandler((source) => { if (source === 'user') { scheduleAutosave() } }) ``` ## Related examples - [Before create/update shape](/examples/events/before-create-update-shape) - Constrain shapes to a circular area - [Before delete shape](/examples/events/before-delete-shape) - Prevent deletion of certain shapes - [After create/update shape](/examples/events/after-create-update-shape) - Ensure only one red shape exists at a time - [After delete shape](/examples/events/after-delete-shape) - Delete empty frames automatically - [Shape meta (on change)](/examples/events/meta-on-change) - Track modification history with a before-change handler