Plate
PlateEditorsTemplates
GitHub16kGitHub
DiscordDiscord
    • Overview
    • Why This Fork
    • Examples
    • Installing Plite
    • Adding Event Handlers
    • Defining Custom Elements
    • Applying Custom Formatting
    • Executing Commands
    • Saving to a Database
    • Canonical Change Substrate
    • Improving Performance
    • Interfaces
    • Nodes
    • Locations
    • Transforms
    • Document Changes
    • Commands
    • Editor
    • Plugins
    • Rendering
    • Serializing
    • Normalizing
    • Using TypeScript
    • Roots
    • Document State
    • Editing Behavior
    • Selection And DOM
    • Clipboard And Paste
    • Decorations, annotations, and widgets
    • Schema
    • 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
    • Plite DOM
    • History Editor API
    • History Plugin Setup
    • History
    • Plite History
    • Plite Hyperscript
    • Plite PaginationExperimental
    • Annotations
    • DOM Coverage Boundaries
    • Editable Component
    • External Text Views
    • Plite React Event Handling
    • Virtualized Rendering
    • Plite React Hooks
    • React Editor Setup
    • React Editor
    • Plite React
    • Plite Component
    • Plite Yjs
    • Plite
    • Migration
    • Contributing
    • 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 "plitejs";
import { Editable, EditorRoot, useEditor } from "plitejs/react";
 
const STORAGE_KEY = "plite.document";
 
const
















































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
documentTitle
=
defineStateField
({
key: "document.title",
initial: () => "Untitled",
persist: { ...valueCodecs.string, version: 1 },
});
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 = useEditor({
plugins: [documentTitle],
initialValue,
});
return (
<EditorRoot
editor={editor}
onCommit={({ commit, editor }) => {
if (
!commit.changed.hasAny("document") &&
commit.dirtyStateKeys.length === 0
) {
return;
}
localStorage.setItem(
STORAGE_KEY,
JSON.stringify(editor.read.value())
);
}}
>
<Editable />
</EditorRoot>
);
};
import { useState } from "react";
import { defineStateField, valueCodecs } from "plitejs";
import { Editable, EditorRoot, useEditor } from "plitejs/react";
 
const STORAGE_KEY = "plite.document";
 
const documentTitle = defineStateField({
  key: "document.title",
  initial: () => "Untitled",
  persist: { ...valueCodecs.string, version: 1 },
});
 
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 = useEditor({
    plugins: [documentTitle],
    initialValue,
  });
 
  return (
    <EditorRoot
      editor={editor}
      onCommit={({ commit, editor }) => {
        if (
          !commit.changed.hasAny("document") &&
          commit.dirtyStateKeys.length === 0
        ) {
          return;
        }
 
        localStorage.setItem(
          STORAGE_KEY,
          JSON.stringify(editor.read.value())
        );
      }}
    >
      <Editable />
    </EditorRoot>
  );
};

editor.read.value() returns the persisted document value:

import type { EditorDocumentValue } from "plitejs";
import type { EditorDocumentValue } from "plitejs";

That shape includes the primary document, every extra root, keyed records, and persistent meta from state fields. Fields without persist stay local to the runtime.

Single-root shortcut

The value in <EditorRoot onValueChange> is the mounted root's block array. Save it directly for a tiny single-root editor with no persistent meta fields.

<EditorRoot
  editor={editor}
  onValueChange={({ value }) => {
    localStorage.setItem("plite.children", JSON.stringify(value));
  }}
>
  <Editable />
</EditorRoot>
<EditorRoot
  editor={editor}
  onValueChange={({ value }) => {
    localStorage.setItem("plite.children", JSON.stringify(value));
  }}
>
  <Editable />
</EditorRoot>

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 = useEditor({
  initialValue: {
    children: [{ type: "paragraph", children: [{ text: "Body" }] }],
    roots: {
      header: [{ type: "paragraph", children: [{ text: "Draft" }] }],
      footer: [{ type: "paragraph", children: [{ text: "Internal" }] }],
    },
  },
});
const editor = useEditor({
  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.

<EditorRoot editor={editor}>
  <Editable aria-label="Header" root="header" />
  <Editable aria-label="Body" />
  <Editable aria-label="Footer" root="footer" />
</EditorRoot>
<EditorRoot editor={editor}>
  <Editable aria-label="Header" root="header" />
  <Editable aria-label="Body" />
  <Editable aria-label="Footer" root="footer" />
</EditorRoot>

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.