Back to Tldraw

History (undo/redo)

apps/docs/content/sdk-features/history.mdx

5.4.07.0 KB
Original Source

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.

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

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:

ModeUndo stackRedo stack
recordAddClear
record-preserveRedoStackAddKeep
ignoreSkipKeep

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

  • Timeline scrubber - A visual timeline that lets users scrub through document history.
  • Store events - Listen to store changes, which is how the history manager tracks modifications.