Shape meta (on change)

Stamp updatedBy and updatedAt metadata on shapes every time they change.

import { TLComponents, TLShape, Tldraw, useEditor, useValue } from 'tldraw'
import 'tldraw/tldraw.css'

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

// [1]
type ShapeWithMyMeta = TLShape & { meta: { updatedBy: string; updatedAt: number } }

function MetaUiHelper() {
	const editor = useEditor()
	// [2]
	const onlySelectedShape = useValue(
		'only selected shape',
		() => editor.getOnlySelectedShape() as ShapeWithMyMeta | null,
		[editor]
	)

	return (
		<pre className="tlui-menu" style={{ margin: 0, padding: 8 }}>
			{onlySelectedShape
				? JSON.stringify(onlySelectedShape.meta, null, '\t')
				: 'Select one shape to see its meta data.'}
		</pre>
	)
}

const components: TLComponents = {
	TopPanel: MetaUiHelper,
}

export default function OnChangeShapeMetaExample() {
	return (
		<div className="tldraw__editor">
			<Tldraw
				persistenceKey="tldraw_change_meta_example"
				components={components}
				onMount={(editor) => {
					// [3]
					editor.getInitialMetaForShape = (_shape) => {
						return {
							updatedBy: editor.user.getExternalId(),
							updatedAt: Date.now(),
						}
					}
					// [4]
					editor.sideEffects.registerBeforeChangeHandler('shape', (_prev, next, source) => {
						if (source !== 'user') return next
						return {
							...next,
							meta: {
								updatedBy: editor.user.getExternalId(),
								updatedAt: Date.now(),
							},
						}
					})
				}}
			/>
		</div>
	)
}

/*
Every shape has a `meta` property for your own JSON data. tldraw stores and syncs it but never
reads it. This example stamps `updatedBy` and `updatedAt` on shapes as they change. See the
shapes docs for more on meta: https://tldraw.dev/docs/shapes#meta

[1]
A shape's `meta` is typed as `JsonObject`. To get useful types for your own fields, intersect
the shape type with the shape of your meta.

[2]
Reading `editor.getOnlySelectedShape()` inside `useValue` re-renders this panel whenever the
selection changes or the selected shape's record changes, which is what makes the meta update
live as you drag.

[3]
`getInitialMetaForShape` is called by `createShapes` for every new shape, and its result is
merged with any `meta` you passed. Replacing it here means new shapes start out with the same
fields the change handler maintains.

[4]
A before-change side effect can return a modified record to be written in place of `next`.
Filtering on `source === 'user'` skips changes that arrived from a remote peer, so in a
multiplayer document you don't overwrite the real author with the local user.
*/

Every shape has a meta property for your own JSON data. This example keeps it up to date with a registerBeforeChangeHandler side effect that rewrites meta on every user-initiated shape change, and replaces editor.getInitialMetaForShape so new shapes start with the same fields. A panel at the top shows the selected shape's meta.

Create a shape, select it, and move or restyle it: the timestamp updates with each change. The handler ignores changes with source: 'remote', so in a multiplayer document a peer's edits keep their own author. For setting meta only at creation time, see the shape meta (on create) example.

Is this page helpful?
Prev
Derived view
Next
Shape meta (on create)