I’m working on a React component that uses Excalidraw, and I want to save and load drawings from localStorage so users don’t lose their work after refreshing the page.
import { Excalidraw } from "@excalidraw/excalidraw";
import "@excalidraw/excalidraw/index.css";
import { useEffect, useState } from "react";
export default function DrawExcalidraw() {
const [elements, setElements] = useState([]);
// Load saved drawing
useEffect(() => {
const savedDrawing = localStorage.getItem("drawing");
if (savedDrawing) {
setElements(JSON.parse(savedDrawing));
}
}, []);
// Save drawing every time elements change
useEffect(() => {
localStorage.setItem("drawing", JSON.stringify(elements));
}, [elements]);
return (
<div style={{ height: "100vh" }}>
<Excalidraw
initialData={{ elements }}
onChange={(updatedElements) => setElements(updatedElements)}
/>
</div>
);
}
The problem
After reloading the page, the canvas is empty even though I can see the drawing data saved in localStorage.
Sometimes I get a JSON parse error or nothing appears on the canvas.
I’m not sure if I should use initialData, onChange, or other Excalidraw APIs like serialize/deserialize.
What I’ve tried
onChange={(elements, appState) => ...} to capture both elements and app state.Verified that localStorage.getItem("drawing") returns valid data.
How can I correctly save and restore Excalidraw drawings from localStorage in a React component?
The issue is that the Excalidraw component uses the elements state value from the initial render cycle as the initialData prop when it mounts. Changing it on any subsequent render cycle has no effect, the Excalidraw component has already been initialized with the [] elements state value.
Use a lazy React state initializer function to read in the localStorage value to be used as the initial elements state value.
Example:
import { Excalidraw } from "@excalidraw/excalidraw";
import "@excalidraw/excalidraw/index.css";
import { useEffect, useState } from "react";
export default function DrawExcalidraw() {
const [elements, setElements] = useState(() => {
// Load saved drawing or return default initial state value
const savedDrawing = localStorage.getItem("drawing");
return savedDrawing ? JSON.parse(savedDrawing)) : [];
});
// Save drawing every time elements change
useEffect(() => {
localStorage.setItem("drawing", JSON.stringify(elements));
}, [elements]);
return (
<div style={{ height: "100vh" }}>
<Excalidraw
initialData={{ elements }}
onChange={(updatedElements) => setElements(updatedElements)}
/>
</div>
);
}
Alternatively you can delay rendering Excalidraw until after the elements state has been initialized.
import { Excalidraw } from "@excalidraw/excalidraw";
import "@excalidraw/excalidraw/index.css";
import { useEffect, useState } from "react";
export default function DrawExcalidraw() {
const [elements, setElements] = useState(undefined);
// Load saved drawing or set to default fallback
useEffect(() => {
const savedDrawing = localStorage.getItem("drawing");
setElements(savedDrawing ? JSON.parse(savedDrawing) : []);
}, []);
// Save drawing every time elements change
useEffect(() => {
localStorage.setItem("drawing", JSON.stringify(elements));
}, [elements]);
if (!elements) {
return null; // or some loading indicator like a spinner/etc.
}
return (
<div style={{ height: "100vh" }}>
<Excalidraw
initialData={{ elements }}
onChange={(updatedElements) => setElements(updatedElements)}
/>
</div>
);
}