Plite documents are JSON. Save the full document value when you need more than the primary editor body, such as extra roots, content roots, document titles, page settings, or other persistent meta fields.
This walkthrough uses Local Storage, but the same shape is what you send to your database.
Create the editor from the saved value, then persist the full document value after committed document or state-field changes.
import { useState } from "react";
import { defineStateField, valueCodecs } from "@platejs/plite";
import { Editable, Plite, usePliteEditor } from "@platejs/plite-react";
const STORAGE_KEY = "plite.document";
const documentTitle
import { useState } from "react";
import { defineStateField, valueCodecs } from "@platejs/plite";
import { Editable, Plite, usePliteEditor } from "@platejs/plite-react";
const STORAGE_KEY = "plite.document";
const documentTitle = defineStateField({
key: "document.title",
initial: () => "Untitled",
persist: valueCodecs.string,
});
const fallbackValue = {
children: [
{
type: "paragraph",
children: [{ text: "A line of text in a paragraph." }],
},
],
meta: {
[documentTitle.key]: documentTitle.serialize("Untitled"),
},
};
const App = () => {
const [initialValue] = useState(() => {
const saved = localStorage.getItem(STORAGE_KEY);
return saved ? JSON.parse(saved) : fallbackValue;
});
const editor = usePliteEditor({
extensions: [documentTitle],
initialValue,
});
return (
<Plite
editor={editor}
onCommit={({ commit, editor }) => {
if (
!commit.changed.has("document") &&
commit.dirtyStateKeys.length === 0
) {
return;
}
localStorage.setItem(
STORAGE_KEY,
JSON.stringify(editor.read.value())
);
}}
>
<Editable />
</Plite>
);
};editor.read.value() returns the persisted document value:
type EditorDocumentValue = {
children: Descendant[];
roots?: Record<string, Descendant[]>;
meta?: Record<string, unknown>;
};type EditorDocumentValue = {
children: Descendant[];
roots?: Record<string, Descendant[]>;
meta?: Record<string, unknown>;
};That shape includes the primary document, every extra root, and persistent meta
from state fields. Fields without a persist codec stay local to the runtime.
The value in <Plite onValueChange> is the mounted root's block array. Save
it directly for a tiny single-root editor with no persistent meta fields.
<Plite
editor={editor}
onValueChange={({ value }) => {
localStorage.setItem("plite.children", JSON.stringify(value));
}}
>
<Editable />
</Plite><Plite
editor={editor}
onValueChange={({ value }) => {
localStorage.setItem("plite.children", JSON.stringify(value));
}}
>
<Editable />
</Plite>Use the full document value once the editor owns extra roots, content roots, or state fields.
Pass initialValue.children plus initialValue.roots when the saved document
owns extra roots.
const editor = usePliteEditor({
initialValue: {
children: [{ type: "paragraph", children: [{ text: "Body" }] }],
roots: {
header: [{ type: "paragraph", children: [{ text: "Draft" }] }],
footer: [{ type: "paragraph", children: [{ text: "Internal" }] }],
},
},
});const editor = usePliteEditor({
initialValue: {
children: [{ type: "paragraph", children: [{ text: "Body" }] }],
roots: {
header: [{ type: "paragraph", children: [{ text: "Draft" }] }],
footer: [{ type: "paragraph", children: [{ text: "Internal" }] }],
},
},
});Render each root with Editable root.
<Plite editor={editor}>
<Editable aria-label="Header" root="header" />
<Editable aria-label="Body" />
<Editable aria-label="Footer" root="footer" />
</Plite><Plite editor={editor}>
<Editable aria-label="Header" root="header" />
<Editable aria-label="Body" />
<Editable aria-label="Footer" root="footer" />
</Plite>See Roots for content roots and root rendering.
Create a new editor with initialValue: nextDocument when the user switches to
a different saved document. That is the clean path for arbitrary persisted
state fields.
Use editor.update.value.replace for an in-place full-document replacement.
editor.update.value.replace({
...nextDocument,
selection: null,
});editor.update.value.replace({
...nextDocument,
selection: null,
});The input is complete. Omitted named roots are removed, omitted persistent
fields return to their declared initial values, and supplied metadata is
decoded through each field's value codec. Pass selection: "start" or
selection: "end" to place the cursor at a document edge. Use tx.roots or
tx.setField only for a targeted partial change.
Comment bodies, permissions, resolved state, and audit events belong to your app or collaboration service. The Plite document can store lightweight ids when comments need to travel with copied content, but the thread data should stay in the comment store.
Use Annotations to render comment anchors from that external store.
Persistence uses editor.read.value() for the whole document. Use value from
onValueChange only for single-root shortcuts, and keep external app data
outside the Plite document unless it is part of the document model.