apps/docs/content/sdk-features/history.mdx
The editor's history system tracks changes to the 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.
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.
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 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.
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 or squashToMark. Creating a mark flushes pending changes onto the undo stack. It doesn't clear the redo stack; the next recorded change does that.
Use Editor#undo and Editor#redo to move through history marks.
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:
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>
)
}
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.
// 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-preserveRedoStackwhen 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
ignorewhen 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.
Bailing reverses changes without adding them to the redo stack. The changes are discarded entirely. Use this when canceling an interaction.
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.
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.
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.
Editor#clearHistory removes all undo and redo entries. Use this when loading new documents or resetting the editor state.
editor.loadSnapshot(snapshot)
editor.clearHistory() // Start with clean history
The history manager listens to 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.