Plate
PlateEditorsTemplates
GitHub16kGitHub
DiscordDiscord
  • Feature Kits
  • Plugin
    • Plugin Methods
    • Plugin Shortcuts
    • Plugin Context
    • Plugin Components
    • Plugin Rules
    • Editing Behavior
    • Plugin Input Rules
  • Editor
    • Editor Methods
    • Controlled Value
  • Authored Changes
  • Performance
  • Static Rendering
  • HTML
  • Markdown
  • Form
  • TypeScript
  • Debugging
  • Unit Testing
  • Browser
  • Troubleshooting
  • Locations
  • Transactions
  • Serializing
  • Roots
  • Document Meta
  • Clipboard and Paste
  • Decorations, annotations, and widgets
  • Schema
  • History
  • Pagination
  • Annotations
  • DOM Coverage
  • External Text Views
  • Virtualized Rendering

History

PreviousNext

Understand the undo and redo batch shape stored by the history plugin.

Plate tracks local undo and redo batches by default. Each batch preserves a canonical document change, effects, and selection.

Saving a document revision and its Comments JSON does not save local undo. Ordinary reload creates a fresh editor with empty undo and redo stacks. Application version history and retained authored contributions have separate lifetimes from this session history.

Usage

import { createEditor } from "platejs";
 
const editor = createEditor();
 
const canUndo = editor.read.history.hasUndo();
const result = await editor.api.history.undo();

SchemaPagination

On This Page

UsageHistory objectStatic methodsHistory.isHistory(value: unknown): value is HistoryHistory.toJSON(editor): HistoryJSONHistory.fromJSON(editor, json): HistoryEditor APIeditor.read.history(): Historyeditor.read.history.hasUndo(): booleaneditor.read.history.hasRedo(): booleaneditor.api.history.undo(): Promise<HistoryResult>editor.api.history.redo(): Promise<HistoryResult>tx.history.restore(history): voidtx.history.skip(): voidtx.history.merge(): voidtx.history.newBatch(): voideditor.update({ history: "skip" }, fn): voideditor.update({ history: "merge" }, fn): voideditor.update({ history: "new-batch" }, fn): voideditor.update.history.restore(history): voidAuthored changesComments and document revisionsControlled previews
Build your editor
Production-ready AI template and reusable components.
Get all-access
editor.update({ history: "skip" }).text.insert("draft");
import { createEditor } from "platejs";
 
const editor = createEditor();
 
const canUndo = editor.read.history.hasUndo();
const result = await editor.api.history.undo();
 
editor.update({ history: "skip" }).text.insert("draft");

undo() and redo() each await one complete replay. Use hasUndo() and hasRedo() for controls, and inspect editor.read.history() when you need the full immutable stacks.

Configure retention and automatic grouping on HistoryPlugin:

import { createEditor, HistoryPlugin } from "platejs";
 
const editor = createEditor({
  plugins: [
    HistoryPlugin.configure({
      initialState: { maxDepth: 100, newBatchDelay: 750 },
    }),
  ],
});
import { createEditor, HistoryPlugin } from "platejs";
 
const editor = createEditor({
  plugins: [
    HistoryPlugin.configure({
      initialState: { maxDepth: 100, newBatchDelay: 750 },
    }),
  ],
});

maxDepth defaults to 100. newBatchDelay defaults to 500 milliseconds. The configured values apply to the next accepted update or replay.

History object

import type {
  DocumentChange,
  EditorEffect,
  EditorSchemaIdentity,
  Selection,
} from "platejs";
 
export interface History {
  readonly redos: readonly Batch[];
  readonly revision: number;
  readonly schema: EditorSchemaIdentity;
  readonly undos: readonly Batch[];
}
 
interface Batch {
  readonly change: DocumentChange;
  readonly effects: readonly EditorEffect[];
  readonly selectionAfter: Selection;
  readonly selectionAfterRoot?: string;
  readonly selectionBefore: Selection;
  readonly selectionBeforeRoot?: string;
}
import type {
  DocumentChange,
  EditorEffect,
  EditorSchemaIdentity,
  Selection,
} from "platejs";
 
export interface History {
  readonly redos: readonly Batch[];
  readonly revision: number;
  readonly schema: EditorSchemaIdentity;
  readonly undos: readonly Batch[];
}
 
interface Batch {
  readonly change: DocumentChange;
  readonly effects: readonly EditorEffect[];




Static methods

History.isHistory(value: unknown): value is History

Returns true if the passed in value is a History object and acts as a type guard.

History.toJSON(editor): HistoryJSON

Encode the installed undo and redo stacks as validated version 4 JSON. The envelope records the editor's schema identity, and each batch stores its canonical DocumentChange, registered effects, and selection envelopes.

This advanced API serializes local document history. It does not serialize the native anchor recovery data or live-session effects needed to restore exact comment coverage and undo local comment creation. Load Comments revisions into a fresh editor with empty local history.

Compatible typing and same-node property updates coalesce from canonical change classifications and changed ranges. Grouping fingerprints are runtime-only; loading persisted history starts a fresh automatic group.

History.fromJSON(editor, json): History

Decode and validate history without mutating the editor. Effect codecs are discovered from installed editor-plugin resources, including state fields and standalone effects. Install the returned immutable value through tx.history.restore(...).

Decoding fails before reading any batch when the persisted schema identity does not match the editor. A matching schema ID and version with a different fingerprint means the schema semantics need a version bump.

import { History } from "platejs/history";
 
const json = History.toJSON(editor);
 
localStorage.setItem("editor.history", JSON.stringify(json));
 
const decoded = History.fromJSON(
  editor,
  JSON.parse(localStorage.getItem("editor.history")!)
);
 
editor.update((tx) => {
  tx.history.restore(decoded);
});
import { History } from "platejs/history";
 
const json = History.toJSON(editor);
 
localStorage.setItem("editor.history", JSON.stringify(json));
 
const decoded = History.fromJSON(
  editor,
  JSON.parse(localStorage.getItem("editor.history")!)
);
 
editor.update((tx) => {
  tx.history.restore(decoded);
});

Schema identity is always derived or named; null is invalid in memory and in version 4 JSON. Schema identity, codec versions, effect keys, built-in selection payloads, and JSON values are validated during decoding. Failed decoding leaves editor state untouched.

Editor API

editor.read.history(): History

Read the current undo and redo stacks.

editor.read.history.hasUndo(): boolean

Return whether the undo branch has a surviving mapped batch.

editor.read.history.hasRedo(): boolean

Return whether the redo branch has a surviving mapped batch.

editor.api.history.undo(): Promise<HistoryResult>

Run one complete update that replays the current undo batch.

editor.api.history.redo(): Promise<HistoryResult>

Run one complete update that replays the current redo batch.

Both methods resolve to { status: "applied" } after a replay and { status: "empty" } when no surviving batch exists. An authored dependency conflict returns { status: "blocked", conflicts }, where conflicts contains the immutable contribution IDs that block replay. A live-session effect can return { status: "blocked", reason } when its external state changed or its mutation failed. A blocked replay leaves the same batch at the branch head. Other failures throw.

Call these services outside editor.read(...) and editor.update(...). A history batch can cover several document roots; replay always applies the complete batch.

tx.history.restore(history): void

Replace both history branches when the surrounding update commits. Commit and snapshot subscribers observe the restored revision through that one update.

tx.history.skip(): void

Do not save the current transaction to history.

tx.history.merge(): void

Merge the current transaction into the previous compatible undo batch.

tx.history.newBatch(): void

Start a fresh undo batch for the current transaction.

editor.update({ history: "skip" }, fn): void

Run one update without saving it to history.

editor.update({ history: "merge" }, fn): void

Run one update that merges into the previous compatible undo batch.

editor.update({ history: "new-batch" }, fn): void

Run one update where the first write starts a fresh history batch, then the rest of the callback merges into that batch.

For one direct write, configure the update facade instead:

editor.update({ history: "skip" }).text.insert("draft");
editor.update({ history: "skip" }).text.insert("draft");

editor.update.history.restore(history): void

Restore decoded history as one direct update.

Authored changes

Install authored when the document needs attributed accepted edits or review proposals. It shares Plate's existing History:

import { authored } from "platejs/authored";
 
const editor = createEditor({
  plugins: [
    authored({ authorId: () => session.user.id, retainHistory: true }),
  ],
});
import { authored } from "platejs/authored";
 
const editor = createEditor({
  plugins: [
    authored({ authorId: () => session.user.id, retainHistory: true }),
  ],
});

Undo and redo reverse local interaction batches, including the authored operation and mapped selection recorded with that batch. An accept or reject decision made as a local document action can be undone. Undo is not a substitute for choosing a review decision through Authored.

Use update.authored.decide(...) for review decisions. Use update.authored.revert(...) to create a new compensating contribution from retained accepted history. The original contribution and author remain in the authored record.

Persist History with History.toJSON only when the application needs local undo stacks after reload. The authored document envelope in editor.read.value() separately persists proposals, decisions, causal operations, and optional retained author history.

See Authored Changes.

Comments and document revisions

Document undo maps comment attachments without rewinding conversation data. Replies, message edits, resolution, reopening, and explicit deletion stay outside document undo. Undoing a suggestion decision preserves its discussion and locates it through the same Authored change ID.

Save editor.read.value() with editor.plugin(CommentsPlugin).api.toJSON() under one application revision and conversation generation. Loading that pair creates a fresh editor with initialComments and an empty local undo stack.

For an old-version preview, join current retained conversations with the selected revision's saved targets. Threads without a target in that revision are unavailable; currently deleted threads remain absent. A version restore creates a new application revision under document-head and conversation-generation checks, then opens a fresh editor. It is not a local undo or an in-place document replacement. See Comments for the target join.

Saved comment ranges do not support a changed Yjs baseline. A state vector alone cannot map missed edits through restored targets, and Yjs admission does not support persisted local undo.

Controlled previews

Render ephemeral previews from local state, decorations, or sidecar UI until the user commits them. Do not mutate document content for a preview and then try to make that preview history later.

Use a local defineStateField without a persistence codec and with history: 'skip' for preview state. Cancel by clearing that field. Accept by clearing the preview and applying the real document edit in one normal update.

const previewReplacement = defineStateField<string | null>({
  key: "local.preview.replacement",
  history: "skip",
  initial: () => null,
});
 
editor.update((tx) => {
  tx.setField(previewReplacement, null);
  tx.text.delete({ at: selectedRange });
  tx.text.insert(acceptedText);
});
const previewReplacement = defineStateField<string | null>({
  key: "local.preview.replacement",
  history: "skip",
  initial: () => null,
});
 
editor.update((tx) => {
  tx.setField(previewReplacement, null);
  tx.text.delete({ at: selectedRange });
  tx.text.insert(acceptedText);
});

Undo then restores the document content without resurrecting preview UI.

readonly selectionAfter: Selection;
readonly selectionAfterRoot?: string;
readonly selectionBefore: Selection;
readonly selectionBeforeRoot?: string;
}