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

Document Meta

PreviousNext

Persist primary children, extra roots, and typed document meta fields in one value.

Plite persists a document as primary children, optional extra roots, and optional meta. Use roots for editable content outside the primary document and state fields for document metadata, settings, and other small model state that should share the editor runtime.

Use external stores for comment bodies, permissions, and audit events; Plite can render their anchors through annotations.

Value Shape

The full persisted value is EditorDocumentValue.

type EditorDocumentValue = {
  children: Descendant[];
  roots?: Record<string, Descendant[]>;
  meta?: Record<string, unknown>;
};
RootsContributing

On This Page

Value ShapeSave The DocumentLoad The DocumentSchema IdentityState FieldsPersistent And Local FieldsCollaborationComments
Build your editor
Production-ready AI template and reusable components.
Get all-access
type EditorDocumentValue = {
  children: Descendant[];
  roots?: Record<string, Descendant[]>;
  meta?: Record<string, unknown>;
};

children is the primary editable body. Extra roots store headers, footers, content roots, synced blocks, captions, and other editable regions owned by the same editor.

meta stores fields whose descriptors define a versioned persist codec. Plite encodes those fields in editor.read.value() and validates their codec version when loading initialValue.

Save The Document

Read the full document value from editor.read.value().

const documentValue = editor.read.value();
 
await saveDocument(JSON.stringify(documentValue));
const documentValue = editor.read.value();
 
await saveDocument(JSON.stringify(documentValue));

The value in <Plite onValueChange> is the mounted root's block array. That is enough for a simple single-root editor, but it drops extra roots and persistent meta fields. Use onCommit at the top-level provider for full document persistence so state-field-only commits and edits in other roots are observed.

<Plite
  editor={editor}
  onCommit={({ commit, editor }) => {
    if (!commit.changed.has("document") && commit.dirtyStateKeys.length === 0) {
      return;
    }
 
    localStorage.setItem(
      "plite.document",
      JSON.stringify(editor.read.value())
    );
  }}
>
  <Editable />
</Plite>
<Plite
  editor={editor}
  onCommit={({ commit, editor }) => {
    if (!commit.changed.has("document") && commit.dirtyStateKeys.length === 0) {
      return;
    }
 
    localStorage.setItem(
      "plite.document",
      JSON.stringify(editor.read.value())
    );
  }}
>
  <Editable />
</Plite>

Load The Document

Pass the saved document value back as initialValue when the editor is created.

const saved = localStorage.getItem("plite.document");
const initialValue = saved
  ? JSON.parse(saved)
  : {
      children: [{ type: "paragraph", children: [{ text: "Body" }] }],
    };
 
const editor = usePliteEditor({
  extensions: [documentTitle],
  initialValue,
});
const saved = localStorage.getItem("plite.document");
const initialValue = saved
  ? JSON.parse(saved)
  : {
      children: [{ type: "paragraph", children: [{ text: "Body" }] }],
    };
 
const editor = usePliteEditor({
  extensions: [documentTitle],
  initialValue,
});

initialValue seeds the editor once. Replace document content later with editor.update, not by changing initialValue props.

A declared schema validates and snapshots initialValue before publication. Unknown element types, roots, properties, invalid property values, and illegal content relationships fail closed. See Schema for explicit validation and canonicalization APIs.

Schema Identity

The document value does not embed its schema. Read the compiled identity when documents can move between schema deployments.

const persisted = {
  document: editor.read.value(),
  schema: editor.read.schema.identity(),
};
const persisted = {
  document: editor.read.value(),
  schema: editor.read.schema.identity(),
};

Every editor returns an identity. A complete schema without id and version returns { kind: "derived", fingerprint }; a named lineage returns { kind: "named", id, version, fingerprint }. Treat a named lineage's version as the application migration boundary and fingerprint as a deterministic check that the same ID/version still describes the same compiled semantics.

@platejs/plite-history stores this identity in History.toJSON(editor) and rejects mismatches in History.fromJSON(editor, json) before decoding any batch. Application document storage owns its own envelope and migration policy.

State Fields

Use defineStateField for document metadata and settings that belong to the editor model.

import { defineStateField, valueCodecs } from "@platejs/plite";
 
const documentTitle = defineStateField({
  key: "document.title",
  collab: "shared",
  history: "push",
  initial: () => "Untitled",
  persist: valueCodecs.string,
});
import { defineStateField, valueCodecs } from "@platejs/plite";
 
const documentTitle = defineStateField({
  key: "document.title",
  collab: "shared",
  history: "push",
  initial: () => "Untitled",
  persist: valueCodecs.string,
});

Read and write state fields through the editor.

const title = editor.read((state) => state.getField(documentTitle));
 
editor.update((tx) => {
  tx.setField(documentTitle, "Q3 Launch Brief");
});
const title = editor.read((state) => state.getField(documentTitle));
 
editor.update((tx) => {
  tx.setField(documentTitle, "Q3 Launch Brief");
});

In React, use useStateFieldValue and useSetStateField for UI controls.

import { useSetStateField, useStateFieldValue } from "@platejs/plite-react";
 
const title = useStateFieldValue(documentTitle);
const setTitle = useSetStateField(documentTitle);
 
return (
  <input value={title} onChange={(event) => setTitle(event.target.value)} />
);
import { useSetStateField, useStateFieldValue } from "@platejs/plite-react";
 
const title = useStateFieldValue(documentTitle);
const setTitle = useSetStateField(documentTitle);
 
return (
  <input value={title} onChange={(event) => setTitle(event.target.value)} />
);

The setter accepts the same typed update policy as its editor. It always adds Plite React's selection-preservation tags, so an external input can control history without stealing focus from itself.

setTitle("Imported title", {
  history: "skip",
  tags: "import",
});
setTitle("Imported title", {
  history: "skip",
  tags: "import",
});

Use state fields for title, layout settings, spellcheck, page settings, or document-level mode flags. Do not store ephemeral UI state there unless it should persist with the document.

Persistent And Local Fields

Omit persist for local runtime fields. Plite keeps the value available through state.getField(field) and omits it from editor.read.value().

const sidePanel = defineStateField({
  key: "ui.side-panel",
  history: "skip",
  initial: () => "closed",
});
const sidePanel = defineStateField({
  key: "ui.side-panel",
  history: "skip",
  initial: () => "closed",
});

Use this for view-only UI state, temporary panels, local drafts, or caches.

Collaboration

State-field writes emit the field's typed effect and list its key in commit.dirtyStateKeys. Collaboration adapters export effects whose descriptor uses collab: "shared"; local effects stay local.

editor.update((tx) => {
  tx.setField(documentTitle, "Remote Q2 Brief");
});
 
const commit = editor.read.lastCommit();
editor.update((tx) => {
  tx.setField(documentTitle, "Remote Q2 Brief");
});
 
const commit = editor.read.lastCommit();

Replay decoded shared effects through tx.effects.emit(...). The field's persist codec also versions the generated transition effect; @platejs/yjs owns the registry and remote policy for Yjs documents.

editor.update((tx) => {
  tx.effects.emit(documentTitle.effect, decodeTitleEffect(message));
});
editor.update((tx) => {
  tx.effects.emit(documentTitle.effect, decodeTitleEffect(message));
});

Keep large shared values outside state fields or define a compact custom effect and reducer. The default field effect carries the previous and next value so history can invert it exactly.

Comments

Comment bodies, permissions, resolved state, and audit events belong to the app or collaboration service. Store a lightweight comment, thread, or annotation id only when the product needs the reference to copy, paste, serialize, or travel with content.

Use Annotations to render external comment anchors in the editor.