javascriptreactjslocal-storagedraw

How can I correctly save and restore Excalidraw drawings from localStorage in a component?


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

Verified that localStorage.getItem("drawing") returns valid data.

How can I correctly save and restore Excalidraw drawings from localStorage in a React component?


Solution

  • Issue

    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.

    Solution Suggestion

    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>
      );
    }