Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/tldraw/tldraw/llms.txt

Use this file to discover all available pages before exploring further.

The Store is a reactive, in-memory database that manages all of tldraw’s state. It stores shapes, pages, bindings, and other records, automatically notifying components when data changes.

Overview

The Store provides:
  • Reactive updates: Components automatically re-render when data changes
  • Schemas: Type-safe record definitions with validation
  • Persistence: Automatic saving to IndexedDB
  • History: Undo/redo with time-travel
  • Computed values: Derived data with automatic dependency tracking
  • Listeners: React to specific changes
  • Migrations: Handle schema changes over time

Store architecture

Records

Everything in the store is a record:
import { TLShape, TLPage, TLBinding, TLAsset } from '@tldraw/tlschema'

// Shape records
const shape: TLShape = {
  id: 'shape:abc123',
  type: 'geo',
  x: 100,
  y: 100,
  rotation: 0,
  props: {
    w: 200,
    h: 100,
    geo: 'rectangle',
    color: 'blue'
  },
  parentId: 'page:page1',
  index: 'a1',
  typeName: 'shape'
}

// Page records
const page: TLPage = {
  id: 'page:page1',
  name: 'Page 1',
  index: 'a1',
  typeName: 'page'
}

// Binding records
const binding: TLBinding = {
  id: 'binding:xyz789',
  type: 'arrow',
  fromId: 'shape:arrow1',
  toId: 'shape:box1',
  props: { /* binding-specific props */ },
  typeName: 'binding'
}

Store instance

The Editor holds a reference to the store:
import { Editor } from '@tldraw/editor'

const editor = new Editor({ /* ... */ })

// Access the store
const store = editor.store

// Get a record
const shape = store.get('shape:abc123')

// Get all records of a type
const allShapes = store.allRecords().filter(r => r.typeName === 'shape')

// Listen to changes
store.listen((entry) => {
  console.log('Changes:', entry.changes)
})

Working with records

Reading records

// Get by ID
const shape = editor.store.get('shape:abc123')

// Get multiple records
const shapes = ['shape:1', 'shape:2', 'shape:3']
  .map(id => editor.store.get(id))
  .filter(Boolean)

// Query all records
const allRecords = editor.store.allRecords()

// Filter by type
const shapes = editor.store.allRecords()
  .filter(r => r.typeName === 'shape')

// Use helper methods
const shape = editor.getShape('shape:abc123')
const shapes = editor.getCurrentPageShapes()

Creating records

import { createShapeId } from '@tldraw/editor'

// Create a shape
const id = createShapeId()

editor.store.put([{
  id,
  type: 'geo',
  x: 100,
  y: 100,
  rotation: 0,
  props: {
    w: 200,
    h: 100,
    geo: 'rectangle'
  },
  parentId: editor.getCurrentPageId(),
  index: 'a1',
  typeName: 'shape'
}])

// Or use Editor convenience method
editor.createShape({
  type: 'geo',
  x: 100,
  y: 100,
  props: { w: 200, h: 100, geo: 'rectangle' }
})

Updating records

// Update using store
const shape = editor.store.get('shape:abc123')
if (shape) {
  editor.store.put([{
    ...shape,
    x: 200,
    y: 200
  }])
}

// Or use Editor method
editor.updateShape({
  id: 'shape:abc123',
  type: 'geo',
  x: 200,
  y: 200
})

// Batch updates
editor.store.put([
  { ...shape1, x: 100 },
  { ...shape2, x: 200 },
  { ...shape3, x: 300 }
])

Deleting records

// Delete using store
editor.store.remove(['shape:abc123', 'shape:def456'])

// Or use Editor method
editor.deleteShapes(['shape:abc123', 'shape:def456'])

Reactivity

The Store uses reactive signals from @tldraw/state:

Using in React components

import { useValue } from '@tldraw/state-react'
import { useEditor } from '@tldraw/editor'

function ShapeList() {
  const editor = useEditor()
  
  // Automatically re-render when shapes change
  const shapes = useValue(
    'shapes',
    () => editor.getCurrentPageShapes(),
    [editor]
  )
  
  return (
    <ul>
      {shapes.map(shape => (
        <li key={shape.id}>{shape.type} at ({shape.x}, {shape.y})</li>
      ))}
    </ul>
  )
}

Computed values

Create derived data that updates automatically:
import { computed } from '@tldraw/state'

const selectedBounds = computed('selected bounds', () => {
  const shapes = editor.getSelectedShapes()
  if (shapes.length === 0) return null
  
  return Box.Common(shapes.map(s => editor.getShapePageBounds(s)))
})

// Get the value
const bounds = selectedBounds.get()

// Use in React
function SelectionInfo() {
  const bounds = useValue(selectedBounds)
  
  if (!bounds) return null
  
  return <div>Selection: {bounds.width} × {bounds.height}</div>
}

Atoms

Create reactive state:
import { atom } from '@tldraw/state'

const hoveredShapeId = atom<string | null>('hovered', null)

// Set value
hoveredShapeId.set('shape:abc123')

// Get value
const id = hoveredShapeId.get()

// Use in React
function HoverIndicator() {
  const id = useValue(hoveredShapeId)
  
  if (!id) return null
  
  const shape = editor.getShape(id)
  return <div>Hovering: {shape?.type}</div>
}

Store listeners

React to changes in the store:
// Listen to all changes
const unlisten = editor.store.listen((entry) => {
  const { changes, source } = entry
  
  // source: 'user' | 'remote'
  console.log('Source:', source)
  
  // Added records
  for (const record of Object.values(changes.added)) {
    console.log('Added:', record)
  }
  
  // Updated records
  for (const [from, to] of Object.values(changes.updated)) {
    console.log('Updated:', from.id, 'from', from, 'to', to)
  }
  
  // Removed records
  for (const record of Object.values(changes.removed)) {
    console.log('Removed:', record)
  }
})

// Stop listening
unlisten()

Filtered listeners

// Listen only to shape changes
const unlisten = editor.store.listen((entry) => {
  const shapeChanges = Object.values(entry.changes.added)
    .concat(Object.values(entry.changes.updated).map(([_, to]) => to))
    .filter(r => r.typeName === 'shape')
  
  if (shapeChanges.length > 0) {
    console.log('Shapes changed:', shapeChanges)
  }
})

Side effects

Run code when specific records change:
import { react } from '@tldraw/state'

// React to selection changes
const dispose = react('selection changed', () => {
  const selectedIds = editor.getSelectedShapeIds()
  console.log('Selection:', selectedIds)
  
  // This will automatically re-run when selection changes
})

// Clean up
dispose()

Computed cache

Cache expensive computations per shape:
// Create a computed cache
const boundsCache = editor.store.createComputedCache(
  'shape bounds',
  (shape: TLShape) => {
    return editor.getShapePageBounds(shape)
  }
)

// Get cached value
const bounds = boundsCache.get(shape.id)

// Cache is automatically invalidated when shape changes

History and transactions

Undo/redo

// Undo
editor.undo()

// Redo
editor.redo()

// Check availability
const canUndo = editor.getCanUndo()
const canRedo = editor.getCanRedo()

// Clear history
editor.history.clear()

Transactions

Group operations into a single history entry:
// Create a mark for history
editor.mark('create shapes')

// Make changes
editor.createShape({ type: 'geo', x: 0, y: 0 })
editor.createShape({ type: 'geo', x: 100, y: 100 })

// Single undo will undo both creates

// Or use run() for atomic operations
editor.run(
  () => {
    editor.createShape({ type: 'geo', x: 0, y: 0 })
    editor.createShape({ type: 'geo', x: 100, y: 100 })
  },
  { history: 'record' }
)

History options

// Record in history (default)
editor.run(() => { /* changes */ }, { 
  history: 'record' 
})

// Ignore history
editor.run(() => { /* changes */ }, { 
  history: 'ignore' 
})

// Record but preserve redo stack
editor.run(() => { /* changes */ }, { 
  history: 'record-preserveRedoStack' 
})

Persistence

Snapshots

Save and restore store state:
import { getSnapshot, loadSnapshot } from '@tldraw/editor'

// Get snapshot
const snapshot = getSnapshot(editor.store)

// Save to localStorage
localStorage.setItem('tldraw-doc', JSON.stringify(snapshot))

// Load snapshot
const savedSnapshot = JSON.parse(localStorage.getItem('tldraw-doc')!)
loadSnapshot(editor.store, savedSnapshot)

Auto-persistence

tldraw automatically persists to IndexedDB:
import { Tldraw, createTLStore, defaultShapeUtils } from 'tldraw'

function App() {
  const [store] = useState(() => {
    return createTLStore({
      shapeUtils: defaultShapeUtils,
      // Persist to IndexedDB with this ID
      persistenceKey: 'my-tldraw-app'
    })
  })
  
  return <Tldraw store={store} />
}

Custom persistence

Implement your own persistence:
const store = createTLStore({ shapeUtils: [...] })

// Listen and save changes
store.listen((entry) => {
  const snapshot = getSnapshot(store)
  
  // Save to your backend
  fetch('/api/save', {
    method: 'POST',
    body: JSON.stringify(snapshot)
  })
})

// Load from backend
fetch('/api/load')
  .then(res => res.json())
  .then(snapshot => loadSnapshot(store, snapshot))

Schema and migrations

Defining schemas

import { T } from '@tldraw/editor'

const myShapeProps = {
  w: T.number,
  h: T.number,
  color: T.string,
  text: T.string
}

Migrations

Handle schema changes over time:
const myShapeMigrations = {
  currentVersion: 2,
  migrators: {
    1: {
      up(shape: any) {
        // Migrate from v0 to v1
        return { ...shape, color: shape.fill || 'black' }
      },
      down(shape: any) {
        // Downgrade from v1 to v0
        const { color, ...rest } = shape
        return { ...rest, fill: color }
      }
    },
    2: {
      up(shape: any) {
        // Migrate from v1 to v2
        return { ...shape, opacity: 1 }
      },
      down(shape: any) {
        // Downgrade from v2 to v1
        const { opacity, ...rest } = shape
        return rest
      }
    }
  }
}

class MyShapeUtil extends ShapeUtil<MyShape> {
  static override props = myShapeProps
  static override migrations = myShapeMigrations
}

Best practices

Use Editor methods: Prefer editor.createShape() over direct store access for most operations. The Editor handles validation and side effects.
Batch updates: Use editor.run() or store.put() with multiple records to batch updates and avoid unnecessary re-renders.
Reactivity: Use useValue() in React components to automatically subscribe to store changes.
Transactions: Wrap related operations in editor.run() to create atomic undo/redo entries.