Files
upstream-tldraw/apps/docs/content/sdk-features/history.mdx
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

170 lines
7.3 KiB
Plaintext

---
title: History (undo/redo)
created_at: 12/20/2025
updated_at: 12/29/2025
keywords:
- undo
- redo
- history
- HistoryManager
- marks
- batch
reviewed_by: steveruizok
status: published
date: 12/20/2025
readability: 9
voice: 8
completeness: 9
accuracy: 10
notes: ''
---
The editor's history system tracks changes to the [store](/sdk-features/store) and provides undo/redo. Changes are organized into batches separated by marks, which act as stopping points, so a complex interaction undoes as one step instead of many.
The history manager captures all user-initiated store changes automatically and batches rapid changes into single undo steps. You control which changes are recorded with history options. The default UI binds undo to Cmd/Ctrl+Z and redo to Cmd/Ctrl+Shift+Z.
## How it works
The history manager maintains two stacks: one for undos and one for redos. Each stack contains entries that are either diffs (record changes) or marks (stopping points).
When you modify the store, the history manager captures the change as a diff. Changes accumulate until you create a mark, then the pending changes are flushed to the undo stack as a single entry. This batching prevents every keystroke or mouse movement from becoming a separate undo step.
```typescript
editor.updateShape({ id: myShapeId, type: 'geo', x: 100, y: 100 })
editor.updateShape({ id: myShapeId, type: 'geo', x: 110, y: 100 })
editor.updateShape({ id: myShapeId, type: 'geo', x: 120, y: 100 })
// All three updates are batched together until a mark is created
```
When you undo, the manager reverses all changes back to the previous mark, moves them to the redo stack, and applies the reversed diff atomically. Redo does the inverse.
## Marks and stopping points
Marks define where undo and redo operations stop. Create marks with [Editor#markHistoryStoppingPoint](?) at the start of user interactions so that complex operations can be undone in one step. The optional name only shows up in the mark id, which is useful for debugging.
```typescript
const markId = editor.markHistoryStoppingPoint('rotate shapes')
editor.rotateShapesBy(editor.getSelectedShapeIds(), Math.PI / 4)
// Undoing will return to this mark
```
Each mark has a unique identifier that you can use with [bailToMark](#bailing) or [squashToMark](#squashing). Creating a mark flushes pending changes onto the undo stack. It doesn't clear the redo stack; the next recorded change does that.
## Basic operations
### Undo and redo
Use [Editor#undo](?) and [Editor#redo](?) to move through history marks.
```typescript
editor.undo() // Reverse to previous mark
editor.redo() // Reapply changes
```
Both methods return the editor instance for chaining.
The [Editor#canUndo](?) and [Editor#canRedo](?) methods are reactive, so you can use them to update UI button states automatically:
```tsx
import { useEditor, useValue } from 'tldraw'
function UndoButton() {
const editor = useEditor()
const canUndo = useValue('canUndo', () => editor.canUndo(), [editor])
return (
<button disabled={!canUndo} onClick={() => editor.undo()}>
Undo
</button>
)
}
```
### Running operations with history options
The [Editor#run](?) method executes a function while controlling how changes affect history. Use it to make changes that don't pollute the undo stack or that preserve the redo stack for special operations.
```typescript
// Ignore changes (don't add to undo stack)
editor.run(
() => {
editor.updateShape({ id: myShapeId, type: 'geo', x: 100 })
},
{ history: 'ignore' }
)
// Record but preserve redo stack
editor.run(
() => {
editor.updateShape({ id: myShapeId, type: 'geo', x: 100 })
},
{ history: 'record-preserveRedoStack' }
)
```
The three history modes are:
| Mode | Undo stack | Redo stack |
| -------------------------- | ---------- | ---------- |
| `record` | Add | Clear |
| `record-preserveRedoStack` | Add | Keep |
| `ignore` | Skip | Keep |
> We use `record-preserveRedoStack` when selecting shapes. This way you can undo, select some shapes, copy them, and then redo back to where you were. The selection goes on the undo stack, but existing redos aren't cleared.
> We use `ignore` when writing your own pointer position for collaborators to see. Where your cursor was doesn't need to be undoable. (Changes that arrive from other users are marked `'remote'` and are never recorded.)
Nested `run` calls keep the outer mode unless they set their own, and no mode is applied while an undo or redo is in progress.
## Advanced features
### Bailing
Bailing reverses changes without adding them to the redo stack. The changes are discarded entirely. Use this when canceling an interaction.
```typescript
const markId = editor.markHistoryStoppingPoint('begin drag')
// User drags shapes around
// User presses escape to cancel
editor.bailToMark(markId) // Roll back and discard all changes since mark
```
[Editor#bail](?) reverts to the most recent mark. [Editor#bailToMark](?) reverts to a specific mark by ID.
> We use bailing while cloning shapes. A user can switch between translating and cloning by pressing or releasing the alt (option) key during a drag. When this changes, we bail on the changes since the interaction started, then apply the new mode's changes.
### Squashing
[Editor#squashToMark](?) combines all changes since a mark into a single undo step. Intermediate marks are removed. This simplifies the undo experience for complex multi-step operations.
```typescript
const markId = editor.markHistoryStoppingPoint('bump shapes')
editor.nudgeShapes(shapes, { x: 10, y: 0 })
editor.nudgeShapes(shapes, { x: 0, y: 10 })
editor.nudgeShapes(shapes, { x: -5, y: -5 })
editor.squashToMark(markId) // All three nudges become one undo step
```
Squashing doesn't change the current state, only how history is organized. If the mark isn't on the undo stack, `squashToMark` logs an error and does nothing.
> We use squashing during image cropping. While the user adjusts the crop, each change is recorded and can be undone individually. When the user exits crop mode, we squash the intermediate changes into one history entry. A single undo restores the image to its state before cropping began.
### Clearing history
[Editor#clearHistory](?) removes all undo and redo entries. Use this when loading new documents or resetting the editor state.
```typescript
editor.loadSnapshot(snapshot)
editor.clearHistory() // Start with clean history
```
## Integration with the store
The history manager listens to [store](/sdk-features/store) changes through a history interceptor. It only captures changes with source `'user'`; changes merged from other clients (source `'remote'`) are ignored, and internal writes that shouldn't be undoable use `history: 'ignore'`. Only store records take part in undo/redo; state held outside the store, like your own atoms, does not.
The three history modes map onto three internal states: `Recording`, `RecordingPreserveRedoStack`, and `Paused`. The manager pauses itself while applying an undo or redo so those writes don't create new entries.
## Related examples
- [Timeline scrubber](/examples/use-cases/timeline-scrubber) - A visual timeline that lets users scrub through document history.
- [Store events](/examples/events/store-events) - Listen to store changes, which is how the history manager tracks modifications.