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 database that manages collections of typed records. It provides automatic updates, history tracking, validation, and efficient querying for tldraw’s data model.

Overview

The Store is the central container for your application’s data, providing:
  • Reactive state management - Automatic updates using signals
  • Type-safe operations - Fully typed record operations
  • History tracking - Change notifications and history entries
  • Schema validation - Built-in validation and migrations
  • Side effects - Lifecycle hooks for business logic
  • Efficient querying - Indexes and reactive queries

Creating a store

import { Store, StoreSchema } from '@tldraw/store'
import { createTLSchema } from '@tldraw/tlschema'

const schema = createTLSchema()

const store = new Store({
  schema,
  props: {}
})

Constructor options

config
object
required
Configuration object for the store
schema
StoreSchema<R, Props>
required
The schema defining record types, validation, and migrations
props
Props
required
Custom properties for the store instance
id
string
Optional unique identifier for the store. Generated automatically if not provided.
initialData
SerializedStore<R>
Initial data to populate the store on creation

Example with initial data

const store = new Store({
  schema,
  props: {},
  initialData: {
    'shape:abc': {
      id: 'shape:abc',
      typeName: 'shape',
      type: 'geo',
      x: 100,
      y: 100,
      props: { w: 100, h: 100 }
    }
  }
})

Core properties

schema

readonly schema: StoreSchema<R, Props>
The schema that defines the structure and validation rules for records in this store.

history

readonly history: Atom<number, RecordsDiff<R>>
An atom containing the store’s history. The history tracks all changes to records.

query

readonly query: StoreQueries<R>
Reactive queries and indexes for efficiently accessing store data.

sideEffects

readonly sideEffects: StoreSideEffects<R>
Side effects manager for registering lifecycle event handlers.

Record operations

get

Get a record by its id.
get<K extends IdOf<R>>(id: K): RecordFromId<K> | undefined
id
IdOf<R>
required
The id of the record to retrieve
record
R | undefined
The record with the given id, or undefined if not found
Example:
const shape = store.get('shape:abc123')
if (shape) {
  console.log('Found shape:', shape.type)
}

has

Check if a record exists.
has<K extends IdOf<R>>(id: K): boolean
Example:
if (store.has('shape:abc123')) {
  console.log('Shape exists')
}

put

Add or update records in the store.
put(records: R[], source?: ChangeSource): void
records
R[]
required
Array of records to add or update
source
'user' | 'remote'
The source of the changes. Defaults to ‘user’.
Example:
store.put([
  {
    id: 'shape:abc',
    typeName: 'shape',
    type: 'geo',
    x: 100,
    y: 100,
    props: { w: 100, h: 100 }
  }
])

update

Update a record using a function.
update<K extends IdOf<R>>(
  id: K, 
  updater: (record: RecordFromId<K>) => RecordFromId<K>
): void
Example:
store.update('shape:abc', (shape) => ({
  ...shape,
  x: shape.x + 10
}))

remove

Remove records from the store.
remove(ids: IdOf<R>[]): void
Example:
store.remove(['shape:abc', 'shape:def'])

Querying

allRecords

Get all records in the store.
allRecords(): R[]
Example:
const allShapes = store.allRecords().filter(r => r.typeName === 'shape')

query.records

Create a reactive query for records of a specific type.
query.records<TypeName extends R['typeName']>(
  typeName: TypeName
): Signal<Extract<R, { typeName: TypeName }>[]>
Example:
const shapesSignal = store.query.records('shape')

// Get current value
const shapes = shapesSignal.get()

// React to changes
react('log shapes', () => {
  console.log('Shapes changed:', shapesSignal.get())
})

createComputedCache

Create a computed cache for expensive calculations on records.
createComputedCache<Data, S extends R = R>(
  name: string,
  derive: (record: S) => Data
): ComputedCache<Data, S>
name
string
required
Name for the cache (for debugging)
derive
(record: S) => Data
required
Function that computes the cached data from a record
cache
ComputedCache<Data, S>
A computed cache instance with a get(id) method
Example:
const boundsCache = store.createComputedCache(
  'bounds',
  (shape: TLShape) => {
    // Expensive calculation
    return calculateBounds(shape)
  }
)

// Get cached bounds (computed once, cached)
const bounds = boundsCache.get('shape:abc')

Change tracking

listen

Listen to changes in the store.
listen(
  onHistory: StoreListener<R>,
  filters?: Partial<StoreListenerFilters>
): () => void
onHistory
(entry: HistoryEntry<R>) => void
required
Callback function called when changes occur
filters
Partial<StoreListenerFilters>
Optional filters to control which changes trigger the listener
source
'user' | 'remote' | 'all'
Filter by the source of changes. Defaults to ‘all’.
scope
'document' | 'session' | 'presence' | 'all'
Filter by record scope. Defaults to ‘all’.
dispose
() => void
Function to remove the listener
Example:
const unlisten = store.listen((entry) => {
  console.log('Changes:', entry.changes)
  console.log('Added:', Object.keys(entry.changes.added))
  console.log('Updated:', Object.keys(entry.changes.updated))
  console.log('Removed:', Object.keys(entry.changes.removed))
})

// Later, stop listening
unlisten()

Listen only to user changes

const unlisten = store.listen(
  (entry) => {
    console.log('User made changes:', entry.changes)
  },
  { source: 'user' }
)

Snapshots and persistence

getSnapshot

Get a snapshot of the store’s current state.
getSnapshot(scope?: RecordScope): StoreSnapshot<R>
scope
'document' | 'session' | 'all'
Which records to include. Defaults to ‘document’.
snapshot
StoreSnapshot<R>
A snapshot containing the store data and schema information
Example:
const snapshot = store.getSnapshot()
localStorage.setItem('drawing', JSON.stringify(snapshot))

loadSnapshot

Load a snapshot into the store.
loadSnapshot(snapshot: StoreSnapshot<R>): void
Example:
const data = localStorage.getItem('drawing')
if (data) {
  const snapshot = JSON.parse(data)
  store.loadSnapshot(snapshot)
}

Side effects

Register callbacks for record lifecycle events.

registerBeforeCreateHandler

store.sideEffects.registerBeforeCreateHandler('shape', (shape) => {
  // Modify shape before it's created
  return { ...shape, meta: { createdAt: Date.now() } }
})

registerAfterCreateHandler

store.sideEffects.registerAfterCreateHandler('shape', (shape) => {
  console.log('Shape created:', shape.id)
})

registerBeforeChangeHandler

store.sideEffects.registerBeforeChangeHandler('shape', (prev, next) => {
  // Validate or modify changes
  if (next.x < 0) {
    return { ...next, x: 0 }
  }
  return next
})

registerAfterChangeHandler

store.sideEffects.registerAfterChangeHandler('shape', (prev, next) => {
  console.log('Shape changed from', prev, 'to', next)
})

registerBeforeDeleteHandler

store.sideEffects.registerBeforeDeleteHandler('shape', (shape) => {
  console.log('About to delete:', shape.id)
})

registerAfterDeleteHandler

store.sideEffects.registerAfterDeleteHandler('shape', (shape) => {
  console.log('Deleted:', shape.id)
})

Transactions

Group multiple operations together.
import { transact } from '@tldraw/state'

transact(() => {
  store.put([shape1, shape2, shape3])
  store.update('shape:abc', (s) => ({ ...s, x: 100 }))
  store.remove(['shape:old'])
})
All changes within the transact callback are batched and listeners are notified once at the end.

Advanced: Migrations

The store automatically handles schema migrations when loading snapshots from older versions.
const snapshot = store.getSnapshot()
// Snapshot includes schema version information

// When loading, migrations are applied automatically
store.loadSnapshot(oldSnapshot)
// Data is migrated to current schema version