The editor reads and writes BlockNote's block JSON. Store it as is in your database. You can pass shorthand when creating content, such as content: "Text" instead of a full text array, and onChange always gives you complete blocks.
[
{
"id" : "c1f…" ,
"type" : "heading" ,
"props" : { "level" : 2 , "textColor" : "default" , "textAlignment" : "left" },
"content" : [{ "type" : "text" , "text" : "Meeting notes" , "styles" : {} }],
"children" : []
}
] Pass initialContent for the first render and read changes from onChange. This is the simplest way to save to your own backend.
Preview Code
import { useState } from "react";
import { BlockEditor, type Block } from "genesis-editor";
export default function UncontrolledExample() {
const [blocks, setBlocks] = useState<Block[]>([]);
return (
<div>
<BlockEditor
initialContent={[
{ type: "heading", props: { level: 2 }, content: "Meeting notes" },
{ type: "bulletListItem", content: "Ship the uploads panel" },
{ type: "bulletListItem", content: "Write the docs" },
]}
onChange={(next) => setBlocks(next)}
/>
<p className="px-15 text-sm text-muted-foreground">
{blocks.length ? `${blocks.length} blocks after your last edit` : "Edit to see onChange fire"}
</p>
</div>
);
}
Pass value and update it from onChange. When value changes to something other than the current document, the editor replaces its content. Changes made through value don't call onChange again.
Preview Code
import { useState } from "react";
import { BlockEditor, type PartialBlock } from "genesis-editor";
const templates: Record<string, PartialBlock[]> = {
Todo: [
{ type: "heading", props: { level: 3 }, content: "Today" },
{ type: "checkListItem", content: "Review pull requests" },
{ type: "checkListItem", content: "Reply to issues" },
],
Quote: [{ type: "quote", content: "Simplicity is prerequisite for reliability." }],
};
export default function ControlledExample() {
const [value, setValue] = useState<PartialBlock[]>(templates.Todo);
return (
<div className="flex flex-col gap-3">
<div className="flex gap-2 px-15">
{Object.keys(templates).map((name) => (
<button
key={name}
className="rounded-md border px-2.5 py-1 text-sm hover:bg-muted"
onClick={() => setValue(templates[name])}
>
Load {name}
</button>
))}
</div>
<BlockEditor
value={value}
onChange={(blocks) => setValue(blocks)}
/>
</div>
);
}
persist saves the document to localStorage shortly after each edit, flushes when the tab is hidden or closed, and restores it on the next visit. Give each document its own key. onPersistStatusChange drives a "Saving…/Saved" indicator.
Preview Code
import { useState } from "react";
import { BlockEditor, type PersistStatus } from "genesis-editor";
export default function PersistExample() {
const [status, setStatus] = useState<PersistStatus>({ status: "idle" });
return (
<div>
<BlockEditor
persist={{ key: "docs:persist-example", debounceMs: 300 }}
onPersistStatusChange={setStatus}
initialContent={[{ type: "paragraph", content: "Type here, then reload the page." }]}
/>
<p className="px-15 text-sm text-muted-foreground">
{status.status === "saved"
? `Saved at ${new Date(status.savedAt).toLocaleTimeString()}`
: status.status === "saving"
? "Saving…"
: "Not saved yet"}
</p>
</div>
);
}
A saved document wins over initialContent. Use the ref's clearPersisted() to forget it.
editable={false} renders the document without the side menu, toolbars, or mobile toolbar.
Preview Code
import { useState } from "react";
import { BlockEditor } from "genesis-editor";
export default function ReadOnlyExample() {
const [editable, setEditable] = useState(false);
return (
<div className="flex flex-col gap-3">
<label className="flex items-center gap-2 px-15 text-sm">
<input type="checkbox" checked={editable} onChange={(e) => setEditable(e.target.checked)} />
Editable
</label>
<BlockEditor
editable={editable}
initialContent={[
{ type: "heading", props: { level: 3 }, content: "Release notes" },
{ type: "paragraph", content: "Read-only documents keep their formatting but hide the editing UI." },
]}
/>
</div>
);
}
A ref gives you getMarkdown(), setMarkdown(), getBlocks(), setBlocks(), setHTML(), importFile(), focus(), and the underlying BlockNote editor. See the API reference .
Preview Code
import { useRef, useState } from "react";
import { BlockEditor, type BlockEditorHandle } from "genesis-editor";
export default function RefExample() {
const editor = useRef<BlockEditorHandle>(null);
const [markdown, setMarkdown] = useState("");
return (
<div className="flex flex-col gap-3">
<BlockEditor
ref={editor}
initialContent={[
{ type: "heading", props: { level: 3 }, content: "Export me" },
{ type: "paragraph", content: [{ type: "text", text: "Bold", styles: { bold: true } }, " and plain text."] },
]}
/>
<div className="px-15">
<button
className="rounded-md border px-2.5 py-1 text-sm hover:bg-muted"
onClick={() => setMarkdown(editor.current?.getMarkdown() ?? "")}
>
Get Markdown
</button>
{markdown && <pre className="mt-3 rounded-md bg-muted p-3 text-xs">{markdown}</pre>}
</div>
</div>
);
}