Files
upstream-mirror a774ac673a
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
2026-09-23 16:12:52 +05:00

128 lines
5.9 KiB
Plaintext

---
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