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.

Tldraw provides built-in persistence capabilities to save and restore the editor state. You can use IndexedDB for local storage or implement custom persistence.

Built-in persistence

Use the persistenceKey prop to enable automatic local storage:
PersistenceKeyExample.tsx
import { Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'

export default function PersistenceKeyExample() {
  return (
    <div className="tldraw__editor">
      <Tldraw persistenceKey="example" />
    </div>
  )
}
With this simple setup:
  • Editor state is automatically saved to IndexedDB
  • State is restored when the component mounts
  • Changes are persisted in real-time
  • Each persistenceKey creates a separate saved document
The persistenceKey should be unique for each document. If you have multiple editors, use different keys for each one.

How it works

When you set a persistenceKey:
  1. On mount, tldraw loads the saved state from IndexedDB
  2. As users make changes, the state is automatically saved
  3. On next visit, the editor restores to the last saved state

Multiple documents

You can manage multiple documents by using different persistence keys:
import { Tldraw } from 'tldraw'
import { useState } from 'react'

const documents = [
  { id: 'doc-1', name: 'Design mockups' },
  { id: 'doc-2', name: 'Flowchart' },
  { id: 'doc-3', name: 'Whiteboard' },
]

export default function MultiDocExample() {
  const [currentDoc, setCurrentDoc] = useState('doc-1')

  return (
    <div>
      <div style={{ padding: 10 }}>
        {documents.map(doc => (
          <button
            key={doc.id}
            onClick={() => setCurrentDoc(doc.id)}
            style={{
              fontWeight: currentDoc === doc.id ? 'bold' : 'normal',
            }}
          >
            {doc.name}
          </button>
        ))}
      </div>
      <div className="tldraw__editor">
        <Tldraw
          key={currentDoc}
          persistenceKey={currentDoc}
        />
      </div>
    </div>
  )
}
Use the key prop to force React to remount the component when switching documents. This ensures the correct state is loaded.

Custom persistence

For more control, implement custom persistence using snapshots:
import { Tldraw, TLStoreSnapshot, createTLStore, defaultShapeUtils } from 'tldraw'
import { useEffect, useState } from 'react'
import 'tldraw/tldraw.css'

export default function CustomPersistenceExample() {
  const [store] = useState(() => createTLStore({ shapeUtils: defaultShapeUtils }))
  const [isLoaded, setIsLoaded] = useState(false)

  useEffect(() => {
    // Load snapshot from your backend
    async function loadSnapshot() {
      const response = await fetch('/api/load-document')
      const snapshot: TLStoreSnapshot = await response.json()
      store.loadSnapshot(snapshot)
      setIsLoaded(true)
    }

    loadSnapshot()
  }, [store])

  useEffect(() => {
    if (!isLoaded) return

    // Save snapshot when changes occur
    const saveSnapshot = () => {
      const snapshot = store.getSnapshot()
      fetch('/api/save-document', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(snapshot),
      })
    }

    // Listen for changes
    const dispose = store.listen(saveSnapshot, { scope: 'document' })
    return dispose
  }, [store, isLoaded])

  if (!isLoaded) {
    return <div>Loading...</div>
  }

  return (
    <div className="tldraw__editor">
      <Tldraw store={store} />
    </div>
  )
}

Snapshots API

Get snapshot

Capture the current editor state:
import { Editor } from 'tldraw'

function saveDocument(editor: Editor) {
  const snapshot = editor.store.getSnapshot()
  // Save snapshot to your backend
  localStorage.setItem('my-document', JSON.stringify(snapshot))
}

Load snapshot

Restore editor state from a snapshot:
import { Editor, TLStoreSnapshot } from 'tldraw'

function loadDocument(editor: Editor) {
  const data = localStorage.getItem('my-document')
  if (data) {
    const snapshot: TLStoreSnapshot = JSON.parse(data)
    editor.store.loadSnapshot(snapshot)
  }
}

Partial snapshots

You can also work with partial snapshots to sync only specific changes:
// Get changes since a specific point
const changes = editor.store.extractingChanges(() => {
  // Make some changes
  editor.createShape({ type: 'geo', x: 0, y: 0 })
})

// Apply changes to another store
otherEditor.store.applyDiff(changes)

Export and import

Export as JSON

import { Editor } from 'tldraw'

function exportToJSON(editor: Editor) {
  const snapshot = editor.store.getSnapshot()
  const json = JSON.stringify(snapshot, null, 2)
  
  // Download as file
  const blob = new Blob([json], { type: 'application/json' })
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = 'drawing.tldr'
  a.click()
}

Import from JSON

import { Editor, TLStoreSnapshot } from 'tldraw'

function importFromJSON(editor: Editor, file: File) {
  const reader = new FileReader()
  reader.onload = (e) => {
    const snapshot: TLStoreSnapshot = JSON.parse(e.target?.result as string)
    editor.store.loadSnapshot(snapshot)
  }
  reader.readAsText(file)
}

Database storage

For server-side persistence:
import { TLStoreSnapshot } from 'tldraw'

// Save to database
async function saveToDatabase(documentId: string, snapshot: TLStoreSnapshot) {
  await db.documents.update({
    where: { id: documentId },
    data: {
      content: snapshot,
      updatedAt: new Date(),
    },
  })
}

// Load from database
async function loadFromDatabase(documentId: string): Promise<TLStoreSnapshot> {
  const document = await db.documents.findUnique({
    where: { id: documentId },
  })
  return document.content
}

Versioning and migrations

Tldraw includes a migration system for handling schema changes:
import { createTLStore, defaultShapeUtils, TLStoreSnapshot } from 'tldraw'

const store = createTLStore({ shapeUtils: defaultShapeUtils })

// Load old snapshot - migrations run automatically
store.loadSnapshot(oldSnapshot)
Migrations are applied automatically when loading snapshots from older versions.

Best practices

Debounce saves

Avoid saving on every change. Debounce save operations to reduce load.

Handle errors

Implement error handling for failed save/load operations.

Version snapshots

Store version information with snapshots for backwards compatibility.

Compress data

Compress snapshots before storing to reduce size.

Debouncing saves

import { useEffect, useRef } from 'react'
import { Editor } from 'tldraw'

function useDebouncedSave(editor: Editor, delay = 1000) {
  const timeoutRef = useRef<number>()

  useEffect(() => {
    const handleChange = () => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current)
      }

      timeoutRef.current = window.setTimeout(() => {
        const snapshot = editor.store.getSnapshot()
        // Save snapshot
        console.log('Saving...', snapshot)
      }, delay)
    }

    const dispose = editor.store.listen(handleChange, { scope: 'document' })
    return () => {
      dispose()
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current)
      }
    }
  }, [editor, delay])
}