Persist to storage

Load and save the document yourself using getSnapshot, loadSnapshot, and store.listen.

import throttle from 'lodash/throttle'
import { useLayoutEffect, useMemo, useState } from 'react'
import { DefaultSpinner, Tldraw, createTLStore, getSnapshot, loadSnapshot } from 'tldraw'
import 'tldraw/tldraw.css'

// There's a guide at the bottom of this file!

const PERSISTENCE_KEY = 'example-3'

export default function LocalStorageExample() {
	// [1]
	const store = useMemo(() => createTLStore(), [])
	// [2]
	const [loadingState, setLoadingState] = useState<
		{ status: 'loading' } | { status: 'ready' } | { status: 'error'; error: string }
	>({
		status: 'loading',
	})
	// [3]
	useLayoutEffect(() => {
		setLoadingState({ status: 'loading' })

		const persistedSnapshot = localStorage.getItem(PERSISTENCE_KEY)

		if (persistedSnapshot) {
			try {
				const snapshot = JSON.parse(persistedSnapshot)
				loadSnapshot(store, snapshot)
				setLoadingState({ status: 'ready' })
			} catch (error: any) {
				setLoadingState({ status: 'error', error: error.message })
			}
		} else {
			setLoadingState({ status: 'ready' })
		}

		// [4]
		const cleanupFn = store.listen(
			throttle(() => {
				const snapshot = getSnapshot(store)
				localStorage.setItem(PERSISTENCE_KEY, JSON.stringify(snapshot))
			}, 500)
		)

		return () => {
			cleanupFn()
		}
	}, [store])

	// [5]
	if (loadingState.status === 'loading') {
		return (
			<div className="tldraw__editor">
				<h2>
					<DefaultSpinner />
				</h2>
			</div>
		)
	}

	if (loadingState.status === 'error') {
		return (
			<div className="tldraw__editor">
				<h2>Error!</h2>
				<p>{loadingState.error}</p>
			</div>
		)
	}

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

/*
If all you want is local persistence, pass a `persistenceKey` to `Tldraw` and it will
save to IndexedDB for you. This example does the load/save loop by hand against
`localStorage` so you can see the pieces, and swap `localStorage` for your own backend.

[1]
Create the store ourselves rather than letting `Tldraw` do it, so we can load a snapshot
into it before the editor mounts. `useMemo` keeps it stable across renders.

[2]
A discriminated union for the loading state keeps the "error" branch from needing an
`error` field on the other states.

[3]
`useLayoutEffect` runs before the first paint, so if there's a saved snapshot the store
is populated before `Tldraw` renders. `loadSnapshot` replaces the store's contents and
runs any migrations needed for an older snapshot.

[4]
`store.listen` fires on every change. Serializing the whole document each time would be
wasteful, so the listener is throttled. The returned function removes the listener when
the component unmounts.

[5]
Render a spinner while loading, an error if the saved data couldn't be parsed, and the
editor once the store is ready.
*/

The persistenceKey prop gives you local persistence for free, but if you want to save to your own backend you need to do the load/save loop yourself. This example does that against localStorage: it creates a store with createTLStore, loads a saved snapshot into it with loadSnapshot before the editor mounts, then uses a throttled store.listen callback to serialize the store with getSnapshot on every change.

Draw something, reload the page, and it will still be there. To reset, clear the example-3 key from local storage in your browser's dev tools.

Is this page helpful?
Prev
Comment notifications
Next
Static assets