Add snapshot prop, examples (#1856)

This PR:
- adds a `snapshot` prop to the <Tldraw> component. It does basically
the same thing as calling `loadSnapshot` after creating the store, but
happens before the editor actually loads.
- adds a largeish example (including a JSON snapshot) to the examples

We have some very complex ways of juggling serialized data between
multiplayer, file formats, and the snapshot APIs. I'd like to see these
simplified, or at least for our documentation to reflect a narrow subset
of all the options available.

The most common questions seem to be:

Q: How do I serialize data?
A: Via the `Editor.getSnapshot()` method

Q: How do I restore serialized data?
A: Via the `Editor.loadSnapshot()` method OR via the `<Tldraw>`
component's `snapshot` prop

The store has an `initialData` constructor prop, however this is quite
complex as the store also requires a schema class instance with which to
migrate the data. In our components (<Tldraw> and <TldrawEditor>) we
were also accepting `initialData`, however we weren't accepting a
schema, and either way I think it's unrealistic to also expect users to
create schemas themselves and pass those in.

AFAIK the `initialData` prop is only used in the file loading, which is
a good example of how complex it looks like to create a schema and
migrate data outside of the components.

### Change Type

- [x] `minor` — New feature
This commit is contained in:
Steve Ruiz 2023-09-08 15:48:55 +01:00 committed by GitHub
parent f21eaeb4d8
commit 0b3e83be52
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 38344 additions and 10 deletions

View file

@ -70,4 +70,9 @@ test.describe('Routes', () => {
await page.goto('http://localhost:5420/persistence')
await page.waitForSelector('.tl-canvas')
})
test('snapshots', async ({ page }) => {
await page.goto('http://localhost:5420/snapshots')
await page.waitForSelector('.tl-canvas')
})
})

View file

@ -0,0 +1,39 @@
import { Tldraw } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css'
import jsonSnapshot from './snapshot.json'
// ^^^
// This snapshot was previously created with `editor.store.getSnapshot()`
// We'll now load this into the editor with `editor.store.loadSnapshot()`.
// Loading it also migrates the snapshot, so even though the snapshot was
// created in the past (potentially a few versions ago) it should load
// successfully.
const LOAD_SNAPSHOT_WITH_INITIAL_DATA = true
export default function SnapshotExample() {
if (LOAD_SNAPSHOT_WITH_INITIAL_DATA) {
// If you want to use the snapshot as the store's initial data, you can do so like this:
return (
<div className="tldraw__editor">
<Tldraw snapshot={jsonSnapshot} />
</div>
)
}
// You can also load the snapshot an existing editor instance afterwards. Note that this
// does not create a new editor, and doesn't change the editor's state or the editor's undo
// history, so you should only ever use this on mount.
return (
<div className="tldraw__editor">
<Tldraw
autoFocus
onMount={(editor) => {
editor.store.loadSnapshot(jsonSnapshot)
}}
/>
</div>
)
}
// Tips:
// Want to migrate a snapshot but not load it? Use `editor.store.migrateSnapshot()`

File diff suppressed because one or more lines are too long

View file

@ -27,6 +27,7 @@ import MultipleExample from './examples/MultipleExample'
import PersistenceExample from './examples/PersistenceExample'
import ScrollExample from './examples/ScrollExample'
import ShapeMetaExample from './examples/ShapeMetaExample'
import SnapshotExample from './examples/SnapshotExample/SnapshotExample'
import StoreEventsExample from './examples/StoreEventsExample'
import UiEventsExample from './examples/UiEventsExample'
import UserPresenceExample from './examples/UserPresenceExample'
@ -135,6 +136,11 @@ export const allExamples: Example[] = [
path: '/persistence',
element: <PersistenceExample />,
},
{
title: 'Snapshots',
path: '/snapshots',
element: <SnapshotExample />,
},
{
title: 'Custom styles',
path: '/custom-styles',

View file

@ -1,6 +1,6 @@
{
"extends": "../../config/tsconfig.base.json",
"include": ["src", "e2e", "./vite.config.ts"],
"include": ["src", "e2e", "./vite.config.ts", "**/*.json"],
"exclude": ["node_modules", "dist", "**/*.css", ".tsbuild*", "./scripts/legacy-translations"],
"compilerOptions": {
"outDir": "./.tsbuild"

View file

@ -31,6 +31,7 @@ import { SerializedStore } from '@tldraw/store';
import { ShapeProps } from '@tldraw/tlschema';
import { Signal } from '@tldraw/state';
import { StoreSchema } from '@tldraw/store';
import { StoreSnapshot } from '@tldraw/store';
import { StyleProp } from '@tldraw/tlschema';
import { TLArrowShape } from '@tldraw/tlschema';
import { TLArrowShapeArrowheadStyle } from '@tldraw/tlschema';
@ -2009,6 +2010,7 @@ export type TldrawEditorProps = TldrawEditorBaseProps & ({
store: TLStore | TLStoreWithStatus;
} | {
store?: undefined;
snapshot?: StoreSnapshot<TLRecord>;
initialData?: SerializedStore<TLRecord>;
persistenceKey?: string;
sessionId?: string;
@ -2607,6 +2609,7 @@ export function useIsEditing(shapeId: TLShapeId): boolean;
export function useLocalStore({ persistenceKey, sessionId, ...rest }: {
persistenceKey?: string;
sessionId?: string;
snapshot?: StoreSnapshot<TLRecord>;
} & TLStoreOptions): TLStoreWithStatus;
// @internal (undocumented)
@ -2630,7 +2633,9 @@ export function useSelectionEvents(handle: TLSelectionHandle): {
};
// @public (undocumented)
export function useTLStore(opts: TLStoreOptions): TLStore;
export function useTLStore(opts: TLStoreOptions & {
snapshot?: StoreSnapshot<TLRecord>;
}): TLStore;
// @public (undocumented)
export function useTransform(ref: React.RefObject<HTMLElement | SVGElement>, x?: number, y?: number, scale?: number, rotate?: number, additionalOffset?: VecLike): void;

View file

@ -1,4 +1,4 @@
import { SerializedStore, Store } from '@tldraw/store'
import { SerializedStore, Store, StoreSnapshot } from '@tldraw/store'
import { TLRecord, TLStore } from '@tldraw/tlschema'
import { Required, annotateError } from '@tldraw/utils'
import React, {
@ -47,6 +47,7 @@ export type TldrawEditorProps = TldrawEditorBaseProps &
}
| {
store?: undefined
snapshot?: StoreSnapshot<TLRecord>
initialData?: SerializedStore<TLRecord>
persistenceKey?: string
sessionId?: string
@ -187,7 +188,7 @@ export const TldrawEditor = memo(function TldrawEditor({
function TldrawEditorWithOwnStore(
props: Required<TldrawEditorProps & { store: undefined; user: TLUser }, 'shapeUtils' | 'tools'>
) {
const { defaultName, initialData, shapeUtils, persistenceKey, sessionId, user } = props
const { defaultName, snapshot, initialData, shapeUtils, persistenceKey, sessionId, user } = props
const syncedStore = useLocalStore({
shapeUtils,
@ -195,6 +196,7 @@ function TldrawEditorWithOwnStore(
persistenceKey,
sessionId,
defaultName,
snapshot,
})
return <TldrawEditorWithLoadingStore {...props} store={syncedStore} user={user} />

View file

@ -1,3 +1,5 @@
import { StoreSnapshot } from '@tldraw/store'
import { TLRecord } from '@tldraw/tlschema'
import { useEffect, useState } from 'react'
import { TLStoreOptions } from '../config/createTLStore'
import { TLStoreWithStatus } from '../utils/sync/StoreWithStatus'
@ -10,7 +12,11 @@ export function useLocalStore({
persistenceKey,
sessionId,
...rest
}: { persistenceKey?: string; sessionId?: string } & TLStoreOptions): TLStoreWithStatus {
}: {
persistenceKey?: string
sessionId?: string
snapshot?: StoreSnapshot<TLRecord>
} & TLStoreOptions): TLStoreWithStatus {
const [state, setState] = useState<{ id: string; storeWithStatus: TLStoreWithStatus } | null>(
null
)

View file

@ -1,9 +1,17 @@
import { StoreSnapshot } from '@tldraw/store'
import { TLRecord } from '@tldraw/tlschema'
import { useEffect, useRef, useState } from 'react'
import { TLStoreOptions, createTLStore } from '../config/createTLStore'
/** @public */
export function useTLStore(opts: TLStoreOptions) {
const [store, setStore] = useState(() => createTLStore(opts))
export function useTLStore(opts: TLStoreOptions & { snapshot?: StoreSnapshot<TLRecord> }) {
const [store, setStore] = useState(() => {
const store = createTLStore(opts)
if (opts.snapshot) {
store.loadSnapshot(opts.snapshot)
}
return store
})
// prev
const ref = useRef(opts)
useEffect(() => void (ref.current = opts))
@ -15,6 +23,9 @@ export function useTLStore(opts: TLStoreOptions) {
)
) {
const newStore = createTLStore(opts)
if (opts.snapshot) {
newStore.loadSnapshot(opts.snapshot)
}
setStore(newStore)
return newStore
}

View file

@ -40,6 +40,7 @@ import { SelectionHandle } from '@tldraw/editor';
import { SerializedSchema } from '@tldraw/editor';
import { ShapeUtil } from '@tldraw/editor';
import { StateNode } from '@tldraw/editor';
import { StoreSnapshot } from '@tldraw/editor';
import { SvgExportContext } from '@tldraw/editor';
import { TLAnyShapeUtilConstructor } from '@tldraw/editor';
import { TLArrowShape } from '@tldraw/editor';
@ -50,7 +51,7 @@ import { TLCancelEvent } from '@tldraw/editor';
import { TLClickEvent } from '@tldraw/editor';
import { TLClickEventInfo } from '@tldraw/editor';
import { TLDefaultSizeStyle } from '@tldraw/editor';
import { TldrawEditorProps } from '@tldraw/editor';
import { TldrawEditorBaseProps } from '@tldraw/editor';
import { TLDrawShape } from '@tldraw/editor';
import { TLDrawShapeSegment } from '@tldraw/editor';
import { TLEmbedShape } from '@tldraw/editor';
@ -80,6 +81,7 @@ import { TLParentId } from '@tldraw/editor';
import { TLPointerEvent } from '@tldraw/editor';
import { TLPointerEventInfo } from '@tldraw/editor';
import { TLPointerEventName } from '@tldraw/editor';
import { TLRecord } from '@tldraw/editor';
import { TLRotationSnapshot } from '@tldraw/editor';
import { TLSchema } from '@tldraw/editor';
import { TLScribble } from '@tldraw/editor';
@ -90,6 +92,7 @@ import { TLShapePartial } from '@tldraw/editor';
import { TLShapeUtilCanvasSvgDef } from '@tldraw/editor';
import { TLShapeUtilFlag } from '@tldraw/editor';
import { TLStore } from '@tldraw/editor';
import { TLStoreWithStatus } from '@tldraw/editor';
import { TLTextShape } from '@tldraw/editor';
import { TLTickEvent } from '@tldraw/editor';
import { TLUnknownShape } from '@tldraw/editor';
@ -1092,7 +1095,15 @@ function Title({ className, children }: {
}): JSX.Element;
// @public (undocumented)
export function Tldraw(props: TldrawEditorProps & TldrawUiProps & Partial<TLExternalContentProps> & {
export function Tldraw(props: TldrawEditorBaseProps & ({
store: TLStore | TLStoreWithStatus;
} | {
store?: undefined;
persistenceKey?: string;
sessionId?: string;
defaultName?: string;
snapshot?: StoreSnapshot<TLRecord>;
}) & TldrawUiProps & Partial<TLExternalContentProps> & {
assetUrls?: RecursivePartial<TLEditorAssetUrls>;
}): JSX.Element;

View file

@ -4,8 +4,13 @@ import {
ErrorScreen,
LoadingScreen,
RecursivePartial,
StoreSnapshot,
TLOnMountHandler,
TLRecord,
TLStore,
TLStoreWithStatus,
TldrawEditor,
TldrawEditorBaseProps,
TldrawEditorProps,
assert,
useEditor,
@ -31,7 +36,22 @@ import { usePreloadAssets } from './utils/usePreloadAssets'
/** @public */
export function Tldraw(
props: TldrawEditorProps &
props: TldrawEditorBaseProps &
(
| {
store: TLStore | TLStoreWithStatus
}
| {
store?: undefined
persistenceKey?: string
sessionId?: string
defaultName?: string
/**
* A snapshot to load for the store's initial data / schema.
*/
snapshot?: StoreSnapshot<TLRecord>
}
) &
TldrawUiProps &
Partial<TLExternalContentProps> & {
/**