From zbeyens. The source code is available on GitHub.

Plate
PlatePliteEditorsTemplates
GitHub16kGitHub
DiscordDiscord
UNPUBLISHED
  • Overview
  • Why This Fork
  • Examples
Walkthroughs
  • Installing Plite
  • Adding Event Handlers
  • Defining Custom Elements
  • Applying Custom Formatting
  • Executing Commands
  • Saving to a Database
  • Canonical Change Substrate
  • Improving Performance
Concepts
  • Interfaces
  • Nodes
  • Locations
  • Transforms
  • Document Changes
  • Commands
  • Editor
  • Extensions
  • Rendering
  • Serializing
  • Normalizing
  • Using TypeScript
  • Roots
  • Document State
  • Editing Behavior
  • Selection And DOM
  • Clipboard And Paste
  • Projection And Overlays
  • Schema
API
  • Anchor API
  • Location API
  • Path API
  • PointEntry API
  • Point API
  • Range API
  • Selection API
  • Location Types APIs
  • Span API
  • Editor
  • Element API
  • NodeEntry API
  • Node API
  • Node Types APIs
  • Text API
  • Debug Value Scrubbing
  • Transforms API
Libraries
  • Plite DOM
  • History Editor API
  • History Extension Setup
  • History
  • Plite History
  • Plite Hyperscript
  • Plite Layout
  • Annotations
  • DOM Coverage Boundaries
  • Editable Component
  • Plite React Event Handling
  • Virtualized Rendering
  • Plite React Hooks
  • React Editor Setup
  • React Editor
  • Plite React
  • Plite Component
  • Plite Yjs
  • Plite
General
  • Migration
  • Contributing
  • Docs Proof Map
  • FAQ
  • Resources

Saving to a Database

PreviousNext

Persist primary children, extra roots, and document meta from the editor runtime.

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.

Save The Full Document

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
















































Executing CommandsCanonical Change Substrate

On This Page

Save The Full DocumentSingle-Root ShortcutLoad Extra RootsReplace Saved ContentStore Comments Separately
Build your editor
Production-ready AI template and reusable components.
Get all-access
=
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>
);
};
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.

Single-Root Shortcut

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.

Load Extra Roots

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.

Replace Saved Content

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.

Store Comments Separately

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.