Fabric.js autosave needs two things: a storage adapter and the autosave option. The engine then tracks unsaved changes and saves while the user edits.
Loading the live demo. Its code is below.
"use client";
import { isDocumentEngineError } from "fabricjs-document-engine";
import {
useDocumentEngine,
useDocumentState,
} from "fabricjs-document-engine/react";
import { createLocalStorage } from "fabricjs-document-engine/storage";
import { CircleIcon, SquareIcon, Trash2Icon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect } from "react";
import {
DemoCanvas,
DemoFrame,
newShape,
seedShapes,
Status,
Toolbar,
ToolButton,
} from "./demo-ui";
import { useFabricCanvas } from "./use-fabric-canvas";
const storage = createLocalStorage({ prefix: "docs-demo-v2:" });
const DOCUMENT_ID = "autosave-demo";
const TONE = {
saved: "ok",
saving: "busy",
unsaved: "idle",
error: "error",
} as const;
export default function AutosaveDemo() {
const t = useTranslations("demos");
const { elementRef, canvas } = useFabricCanvas();
const engine = useDocumentEngine(canvas, {
storage,
autosave: { delay: 600 },
document: { id: DOCUMENT_ID },
});
const state = useDocumentState(engine);
// Open the saved drawing. On a first visit, start from a small one.
useEffect(() => {
engine?.load(DOCUMENT_ID).catch((error) => {
if (
!isDocumentEngineError(error) ||
error.code !== "DOCUMENT_NOT_FOUND"
) {
console.error(error);
return;
}
engine.canvas.add(...seedShapes());
engine.clearHistory();
});
}, [engine]);
// "conflict" cannot happen with one tab, so it shares the error line.
const status =
state?.saveStatus === "conflict" ? "error" : (state?.saveStatus ?? "saved");
return (
<DemoFrame>
<DemoCanvas elementRef={elementRef} label={t("canvas")} />
<Toolbar>
<ToolButton
icon={SquareIcon}
label={t("square")}
onClick={() => canvas?.add(newShape("square"))}
/>
<ToolButton
icon={CircleIcon}
label={t("circle")}
onClick={() => canvas?.add(newShape("circle"))}
/>
<ToolButton
icon={Trash2Icon}
label={t("clear")}
onClick={() => canvas?.remove(...canvas.getObjects())}
/>
</Toolbar>
<Status tone={TONE[status]}>
{t(`autosave.${status}`, { revision: state?.revision ?? 0 })}
</Status>
</DemoFrame>
);
}Turn it on
import { createDocumentEngine } from "fabricjs-document-engine";
import { createLocalStorage } from "fabricjs-document-engine/storage";
const engine = createDocumentEngine({
canvas,
storage: createLocalStorage(),
autosave: { delay: 1000, maxWait: 10000 },
});delay: save after this many milliseconds without an edit.maxWait: save at the latest this long after the first unsaved edit, even while the user keeps editing.
autosave: true uses these defaults.
What counts as unsaved changes
Every recorded undo step, every undo and redo, and every updateMetadata() call mark the document as changed. engine.isDirty() tells you whether anything is unsaved. Edits made while a save is running stay unsaved until the next save.
Changes your code makes with object.set() fire no Fabric.js event. Wrap them in engine.transaction() so they count, as explained in undo and redo.
Show a save status
The save:status event carries the whole state:
engine.on("save:status", ({ status, lastSavedAt, error }) => {
statusLabel.textContent = status;
});status is one of saved, unsaved, saving, error or conflict. In React, useDocumentState(engine).saveStatus gives you the same value. Keep the status text in an aria-live region, as the accessibility guide shows.
One save at a time
Calling save() while a save is running queues exactly one follow-up save with the latest content. Responses can never arrive out of order, so a slow old save never overwrites newer work. If the user opens another document during a save, the response of that save is ignored.
Retry failed saves
Temporary failures, such as a dropped connection, are retried with exponential backoff:
createDocumentEngine({
canvas,
storage,
autosave: true,
saveRetry: { attempts: 3, baseDelay: 500, maxDelay: 8000 },
});
engine.on("save:retry", ({ attempt, delay }) => {
console.info(`Retry ${attempt} in ${delay} ms`);
});Throw an error with retryable: false from your adapter when trying again cannot help, such as for a permission error. Conflicts are never retried.
Warn before leaving
Fabric.js autosave waits for a pause, so a user can close the tab a second after an edit. Ask the browser to confirm first:
import { bindUnsavedChangesWarning } from "fabricjs-document-engine";
const unbind = bindUnsavedChangesWarning(engine);For work that still gets lost, such as a crash, add recovery.