Persistence
Stream serializedData to a Store or a database and restore a scene later via updateScene.
Overview
serializedData gives you the canonical Excalidraw envelope on every change — stream it to a dcc.Store, write it back to a DB, restore it via command with type='updateScene'.
Live demo
Source
# File: docs/persistence/persistence.py
"""Persistence: save to dcc.Store and restore on reload."""
import json
import uuid
import dash
import dash_mantine_components as dmc
from dash import Input, Output, State, callback, dcc, no_update
from dash_excalidraw import DashExcalidraw
from docs._shared import canvas_frame, code_block, sync_canvas_theme
sync_canvas_theme("persistence-canvas")
CODE = """
# 1. observe serializedData
@callback(Output('store', 'data'),
Input('canvas', 'serializedData'),
prevent_initial_call=True)
def save(snapshot):
return snapshot
# 2. on reload, hand the stored snapshot back as a command
@callback(Output('canvas', 'command'),
Input('btn-restore', 'n_clicks'),
State('store', 'data'),
prevent_initial_call=True)
def restore(_, snapshot):
data = json.loads(snapshot)
return {'id': str(uuid.uuid4()),
'type': 'updateScene',
'payload': {'elements': data['elements'],
'appState': data.get('appState', {})}}
"""
component = dmc.Stack(
gap="md",
children=[
code_block(CODE),
dcc.Store(id="persistence-store", storage_type="session"),
dmc.Group(
[
dmc.Badge(
id="persistence-indicator",
children="Waiting for changes",
color="gray",
variant="light",
),
dmc.Button(
"Restore from store",
id="persistence-restore-btn",
variant="light",
color="teal",
),
dmc.Button(
"Clear store",
id="persistence-clear-btn",
variant="subtle",
color="red",
),
],
justify="flex-start",
),
canvas_frame(
DashExcalidraw(
id="persistence-canvas",
height="600px",
)
),
],
)
@callback(
Output("persistence-store", "data"),
Output("persistence-indicator", "children"),
Output("persistence-indicator", "color"),
Input("persistence-canvas", "serializedData"),
Input("persistence-clear-btn", "n_clicks"),
prevent_initial_call=True,
)
def _save(serialized, _clear_clicks):
trigger = dash.ctx.triggered_id
if trigger == "persistence-clear-btn":
return None, "Cleared", "red"
if not serialized:
return no_update, "No data yet", "gray"
# Excalidraw fires onChange once on mount with an empty scene.
# Without this guard, that first emit would clobber whatever the
# store had from a previous session the moment the page loaded —
# then clicking Restore would hand back an empty canvas.
try:
parsed = json.loads(serialized)
except (TypeError, ValueError):
return no_update, "Invalid snapshot", "yellow"
if not parsed.get("elements"):
return no_update, "Waiting for content…", "gray"
return serialized, "Saved", "green"
@callback(
Output("persistence-canvas", "command"),
Input("persistence-restore-btn", "n_clicks"),
State("persistence-store", "data"),
prevent_initial_call=True,
)
def _restore(_clicks, snapshot):
if not snapshot:
return no_update
try:
parsed = json.loads(snapshot)
except (TypeError, ValueError):
return no_update
return {
"id": f"restore-{uuid.uuid4()}",
"type": "updateScene",
"payload": {
"elements": parsed.get("elements", []),
"appState": parsed.get("appState", {}),
},
}
:defaultExpanded: false :withExpandedButton: true
Source: /persistence
Note for AI agents: This is the static, prerendered view of an interactive Dash application served because we detected a non-JS user agent. Full prose docs:
- /persistence/llms.txt — LLM-friendly documentation
- /sitemap.xml
- /robots.txt