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.
import { createEditor } from "platejs";
const editor = createEditor();
const canUndo = editor.read.history.hasUndo();
const result = await editor.api.history.undo();
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.
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[];
Returns true if the passed in value is a History object and acts as a
type guard.
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.
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.
Read the current undo and redo stacks.
Return whether the undo branch has a surviving mapped batch.
Return whether the redo branch has a surviving mapped batch.
Run one complete update that replays the current undo batch.
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.
Replace both history branches when the surrounding update commits. Commit and snapshot subscribers observe the restored revision through that one update.
Do not save the current transaction to history.
Merge the current transaction into the previous compatible undo batch.
Start a fresh undo batch for the current transaction.
Run one update without saving it to history.
Run one update that merges into the previous compatible undo batch.
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");Restore decoded history as one direct update.
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.
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.
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.