Tldraw/apps/docs/content/docs/persistence.mdx

412 wiersze
12 KiB
Markdown

---
title: Persistence
status: published
author: steveruizok
date: 3/22/2023
order: 6
keywords:
- data
- sync
- persistence
- database
- indexeddb
- localstorage
---
Persistence in tldraw means storing information about the editor's state to a database and then restoring it later. There are a few options that developers have for getting data into tldraw and out again.
## The `"persistenceKey"` prop
Both the `<Tldraw>` or `<TldrawEditor>` components support local persistence and cross-tab synchronization via the `persistenceKey` prop. Passing a value to this prop will persist the contents of the editor locally to the browser's IndexedDb.
```tsx
import { Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
export default function () {
return (
<div style={{ position: 'fixed', inset: 0 }}>
<Tldraw persistenceKey="my-persistence-key" />
</div>
)
}
```
Using a `persistenceKey` will synchronize data automatically with any other tldraw component with the same `persistenceKey` prop, even if that component is in a different browser tab.
```tsx
import { Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
export default function () {
return (
<div style={{ position: 'fixed', inset: 0 }}>
<div style={{ width: '50%', height: '100%' }}>
<Tldraw persistenceKey="my-persistence-key" />
</div>
<div style={{ width: '50%', height: '100%' }}>
<Tldraw persistenceKey="my-persistence-key" />
</div>
</div>
)
}
```
In the example above, both editors would synchronize their document locally. They would still have two independent instance states (e.g. selections) but the document would be kept in sync and persisted under the same key.
## Document Snapshots
You can get a JSON snapshot of the editor's content using the [Editor#store](?)'s [Store#getSnapshot](?) method.
```tsx
function SaveButton() {
const editor = useEditor()
return (
<button
onClick={() => {
const snapshot = editor.store.getSnapshot()
const stringified = JSON.stringify(snapshot)
localStorage.setItem('my-editor-snapshot', stringified)
}}
>
Save
</button>
)
}
```
You can load the snapshot into a new editor with [Store#loadSnapshot](?).
```tsx
function LoadButton() {
const editor = useEditor()
return (
<button
onClick={() => {
const stringified = localStorage.getItem('my-editor-snapshot')
const snapshot = JSON.parse(stringified)
editor.store.loadSnapshot(snapshot)
}}
>
Load
</button>
)
}
```
A [snapshot](/reference/store/StoreSnapshot) includes both the store's [serialized records](/reference/store/SerializedStore) and its [serialized schema](/reference/store/SerializedSchema), which is used for migrations.
> By default, the `getSnapshot` method returns only the editor's document data. If you want to get records from a different scope, you can pass in `session`, `document`, `presence`, or else `all` for all scopes.
Note that loading a snapshot does not reset the editor's in memory state or UI state. For example, loading a snapshot during a resizing operation may lead to a crash. This is because the resizing state maintains its own cache of information about which shapes it is resizing, and its possible that those shapes may no longer exist!
## The `"store"` prop
While it's possible to load the editor and then load data into its store, we've found it best to create the store, set its data, and then pass the store into the editor.
The `store` property of the `<Tldraw>` / `<TldrawEditor>` components accepts a store that you've defined outside of the component.
```tsx
export default function () {
const [store] = useState(() => {
// Create the store
const newStore = createTLStore({
shapeUtils: defaultShapeUtils,
})
// Get the snapshot
const stringified = localStorage.getItem('my-editor-snapshot')
const snapshot = JSON.parse(stringified)
// Load the snapshot
newStore.loadSnapshot(snapshot)
return newStore
})
return <Tldraw persistenceKey="my-persistence-key" store={store} />
}
```
Sometimes you won't be able to access the store's data synchronously. To handle this case, the `store` property also accepts a [TLStoreWithStatus](?).
```tsx
export default function () {
const [storeWithStatus, setStoreWithStatus] = useState<TLStoreWithStatus>({
status: 'loading',
})
useEffect(() => {
let cancelled = false
async function loadRemoteSnapshot() {
// Get the snapshot
const snapshot = await getRemoteSnapshot()
if (cancelled) return
// Create the store
const newStore = createTLStore({
shapeUtils: defaultShapeUtils,
})
// Load the snapshot
newStore.loadSnapshot(snapshot)
// Update the store with status
setStoreWithStatus({
store: newStore,
status: 'ready',
})
}
loadRemoteSnapshot()
return () => {
cancelled = true
}
})
return <Tldraw persistenceKey="my-persistence-key" store={storeWithStatus} />
}
```
For a good example of this pattern, see the [yjs-example](https://github.com/tldraw/tldraw-yjs-example).
## Listening for changes
You can listen for incremental updates to the document state by calling `editor.store.listen`, e.g.
```ts
const unlisten = editor.store.listen(
(update) => {
console.log('update', update)
},
{ scope: 'document', source: 'user' }
)
```
These updates contain information about which records were added, removed, and updated. See [HistoryEntry](?)
The `scope` filter can be used to listen for changes to a specific record scope, e.g. `document`, `session`, `presence`, or `all`.
The `source` filter can be used to listen for changes from a specific source, e.g. `user`, `remote`, or `all`. (See [Store#mergeRemoteChanges](?) for more information on remote changes.)
Note that these incremental updates do not include the schema version. You should make sure that you keep a record of the latest schema version for your snapshots.
You can get the schema version by calling `editor.store.schema.serialize()` and the returned value can replace the `schema` property in the snapshot next time you need to load a snapshot. The schema does not change at runtime so you only need to do this once per session.
## Handling remote changes
If you need to synchronize changes from a remote source, e.g. a multiplayer backend, you can use the `editor.store.mergeRemoteChanges` method. This will 'tag' the changes with the `source` property as `'remote'` so you can filter them out when listening for changes.
```ts
myRemoteSource.on('change', (changes) => {
editor.store.mergeRemoteChanges(() => {
changes.forEach((change) => {
// Apply the changes to the store
editor.store.put(/* ... */)
})
})
})
```
## Migrations
Tldraw uses migrations to bring data from old snapshots up to date. These run automatically when calling `editor.store.loadSnapshot`.
### Running migrations manually
If you need to run migrations on a snapshot without loading it into the store, you can call [StoreSchema#migrateStoreSnapshot](?) directly.
```ts
import { createTLSchema } from 'tldraw'
const snapshot = await getSnapshotFromSomewhere()
const migrationResult = createTLSchema().migrateStoreSnapshot(snapshot)
if (migrationResult.type === 'success') {
console.log('Migrated snapshot', migrationResult.value)
} else {
console.error('Migration failed', migrationResult.reason)
}
```
### Custom migrations
Tldraw supports a couple of ways of adding custom data types to the tldraw store:
- [Custom shape types](/docs/shapes#Custom-shapes-1)
- [`meta` properties](/docs/shapes#Meta-information) on all of our built-in record types.
You might wish to migrate your custom data types over time as you make changes to them.
To enable this, tldraw provides two ways to add custom migrations:
1. **Shape props migrations**, specifically for migrating the shape.props objects on your custom shape types.
2. **The `migrations` config option**, which is more general purpose but much less commonly needed. This will allow you to migrate any data in the store.
#### Shape props migrations
If you have a custom shape type, you can define a `migrations` property on the shape util class. Use the `createShapePropsMigrationSequence` helper to define this property.
```ts
import { createShapePropsMigrationSequence, createShapePropsMigrationIds, ShapeUtil } from 'tldraw'
// Migrations must start a 1 and be sequential integers.
const Versions = createShapePropMigrationIds('custom-shape', {
AddColor: 1,
})
class MyCustomShapeUtil extends ShapeUtil {
static type = 'custom-shape'
static migrations = createShapePropsMigrationSequence({
sequence: [
{
id: Versions.AddColor,
up(props) {
// set the default color
props.color = 'black'
},
},
],
})
// ...
}
```
#### The `migrations` config option
First create a set of migration ids.
```ts
import { createMigrationIds } from 'tldraw'
// The first argument is a unique namespace for your migration sequence.
// We recommend using a reverse domain name, e.g. we use 'com.tldraw.foo.bar'
const SEQUENCE_ID = 'com.example.my-app'
const Versions = createMigrationIds(SEQUENCE_ID, {
// Migrations must start at 1 and be sequential integers.
AddColor: 1,
})
```
Then create a migration sequence.
```ts
import { createMigrationSequence, isShape } from 'tldraw'
const myMigrations = createMigrationSequence({
sequenceId: SEQUENCE_ID,
sequence: [
{
id: Versions.AddColor,
// Scope can be one of
// - 'store' to have the up function called on the whole snapshot at once
// - 'record' to have the up function called on each record individually
scope: 'record',
// if scope is 'record', you can filter which records the migration runs on
filter: (record) => isShape(record) && record.type === 'custom-shape',
up(record) {
record.props.color = 'black'
},
},
],
})
```
And finally pass your migrations in to tldraw via the `migrations` config option. There are a few places where you might need to do this, depending on how specialized your usage of Tldraw is:
```tsx
// When rendering the Tldraw component
<Tldraw
...
migrations={[myMigrations]}
/>
// or when creating the store
store = createTLStore({
...
migrations: [myMigrations],
})
// or when creating the schema
schema = createTLSchema({
...
migrations: [myMigrations],
})
```
### Updating legacy shape migrations (defineMigrations)
You can convert your legacy migrations to the new migrations format by the following process:
1. Wrap your version numbers in `createShapePropsMigrationIds`
```diff
- const Versions = {
+ const Versions = createShapePropMigrationIds('custom-shape', {
AddColor: 1
- }
+ })
```
2. Replace your `defineMigrations` call with `createShapePropsMigrationSequence`
```ts
const migrations = defineMigrations({
currentVersion: Versions.AddColor,
migrators: {
[Versions.AddColor]: {
up: (shape: any) => ({ ...shape, props: { ...shape.props, color: 'black' } }),
down: ({ props: { color, ...props }, ...shape }: any) => ({ ...shape, props }),
},
},
})
```
Becomes
```ts
const migrations = createShapePropsMigrationSequence({
sequence: [
{
id: Versions.AddColor,
// [!!!] You no longer have access to the top-level shape object.
// Only the shape.props object is passed in to the migrator function.
up(props) {
// [!!!] You no longer need to return a new copy of the shape object.
// Instead, you can modify the props object in place.
props.color = 'black'
},
// [!!!] You no longer need to specify a down migration.
},
],
})
```
## Examples
### Local persistence
Tldraw ships with a local-only sync engine based on `IndexedDb` and `BroadcastChannel` called [`TLLocalSyncClient`](https://github.com/tldraw/tldraw/blob/main/packages/editor/src/lib/utils/sync/TLLocalSyncClient.ts).
### Tldraw.com sync engine
[tldraw.com/r](https://tldraw.com/r) currently uses a simple custom sync engine based on a push/pull/rebase-style algorithm.
It can be found [here](https://github.com/tldraw/tldraw/tree/main/packages/tlsync).
It was optimized for Cloudflare workers with [DurableObjects](https://developers.cloudflare.com/durable-objects/)
We don't suggest using our code directly yet, but it may serve as a good reference for your own sync engine.
### Yjs sync example
We created a [tldraw-yjs example](https://github.com/tldraw/tldraw-yjs-example) to illustrate a way of using yjs with the tldraw SDK.
### Shape props migrations example
Our [custom-config example](/examples/shapes/tools/custom-config) shows how to add custom shape props migrations to the tldraw store.
### Meta properties migrations example
Our [custom-config example](/examples/shapes/tools/custom-config) shows how to add custom migrations to the tldraw store.