Plate
PlateEditorsTemplates
GitHub16kGitHub
DiscordDiscord
  • HTML Export
  • Export
  • Server-Side
  • Version History
  • Editable Voids
  • Huge Document
  • Hundreds Editors
  • Markdown Streaming
  • Preview Markdown
  • Collaboration Demo
  • Table Nomerge Demo
  • Excalidraw Demo
  • Single Block Demo
  • Find Demo
  • Lint Demo
  • AI Demo
  • Align Demo
  • Autoformat Demo
  • Basic Nodes Demo
  • Block Menu Demo
  • Node Selection Demo
  • Column Demo
  • Code Block Demo
  • Callout Demo
  • Discussion Demo
  • Date Demo
  • Footnote Demo
  • Drag & Drop Demo
  • Emoji Demo
  • Equation Demo
  • Exit Break Demo
  • Floating Toolbar Demo
  • Font Demo
  • Indent Demo
  • List Demo
  • Line Height Demo
  • Link Demo
  • Media Demo
  • Mention Demo
  • Block Placeholder Demo
  • Serializing Docx Demo
  • Serializing HTML Demo
  • Serializing Markdown Demo
  • Slash Command Demo
  • Plugin Rules Demo
  • Table Demo
  • Table of Contents Demo
  • Details Demo

Version History

PreviousNext

Save document revisions, compare them, and restore one.

This example saves snapshots of the editor's document as revisions. Edit the text, save a revision to compare it with the previous one, or restore any saved revision.

For application revision previews and restores with comments, see Comments and versions.

Demo

Loading…
Server-SideEditable Voids Demo

On This Page

DemoSourceUsageCompare saved revisionsRestore a revisionPersist revisions
Build your editor
Production-ready AI template and reusable components.
Get all-access

Source

'use client';
 
import type { EditorStateSchemaApi, Value } from 'platejs';
import { compare, type TwoWayComparison } from 'platejs/diff';
import {
  EditorRoot,
  EditorContent,
  useCreateEditor,
  useEditor,
} from 'platejs/react';
import * as React from 'react';
 
import { Button } from '@/components/ui/button';
import { BasicMarksKit } from '@/components/editor/basic-marks';
import { Diff } from '@/components/editor/diff';
 
type Revision = ReturnType<ReturnType<typeof useEditor>['read']['value']>;
type SavedRevision = Readonly<{ id: number; value: Revision }>;
 
const initialValue: Value = [
  {
    children: [{ text: 'Save a revision after each edit to compare them.' }],
    type: 'paragraph',
  },
  {
    children: [{ text: 'Edit this text, then save a revision.' }],
    type: 'paragraph',
  },
];
 
export function VersionDiff({
  after,
  before,
  schema,
}: {
  after: unknown;
  before: unknown;
  schema: EditorStateSchemaApi;
}) {
  const [result, setResult] = React.useState<{
    after: unknown;
    comparison: TwoWayComparison | null;
    error: string | null;
  } | null>(null);
 
  React.useEffect(() => {
    const controller = new AbortController();
 
    void compare({ after, before, schema, signal: controller.signal }).then(
      (comparison) => {
        if (!controller.signal.aborted) {
          setResult({ after, comparison, error: null });
        }
      },
      (error: unknown) => {
        if (
          !controller.signal.aborted &&
          !(error instanceof Error && error.name === 'AbortError')
        ) {
          setResult({
            after,
            comparison: null,
            error:
              error instanceof Error ? error.message : 'Comparison failed.',
          });
        }
      }
    );
 
    return () => controller.abort();
  }, [after, before, schema]);
 
  if (result && result.after === after && result.error) {
    return (
      <p className="rounded-md border border-red-300 bg-red-50 p-3 text-sm text-red-950">
        {result.error}
      </p>
    );
  }
  if (!result || result.after !== after || !result.comparison) {
    return (
      <p aria-live="polite" className="rounded-md border p-3 text-sm">
        Comparing revisions…
      </p>
    );
  }
 
  return <Diff comparison={result.comparison} />;
}
 
function RevisionHistory({
  onSave,
  revisions,
}: {
  onSave: (revision: Revision) => void;
  revisions: readonly SavedRevision[];
}) {
  const editor = useEditor();
 
  return (
    <section className="space-y-2">
      <div className="flex items-center justify-between gap-3">
        <h2 className="font-medium">Saved revisions</h2>
        <Button
          onClick={() => onSave(structuredClone(editor.read.value()))}
          size="sm"
          variant="outline"
        >
          Save revision
        </Button>
      </div>
      <p className="text-sm text-muted-foreground">
        Edit the document and save a revision to compare it with the previous
        snapshot, or restore any saved revision.
      </p>
      <ol className="space-y-1 text-sm">
        {revisions.map(({ id, value }, index) => (
          <li className="flex items-center justify-between gap-3" key={id}>
            <span>Revision {index + 1}</span>
            <Button
              aria-label={`Restore revision ${index + 1}`}
              onClick={() => editor.update((tx) => tx.value.replace(value))}
              size="sm"
              variant="ghost"
            >
              Restore
            </Button>
          </li>
        ))}
      </ol>
      {revisions.length > 1 && (
        <VersionDiff
          after={revisions.at(-1)?.value}
          before={revisions.at(-2)?.value}
          schema={editor.read.schema}
        />
      )}
    </section>
  );
}
 
export default function VersionHistoryDemo() {
  const editor = useCreateEditor({
    plugins: BasicMarksKit,
    initialValue,
  });
  const [revisions, setRevisions] = React.useState<readonly SavedRevision[]>(
    () => [{ id: 0, value: structuredClone(editor.read.value()) }]
  );
 
  return (
    <div className="flex flex-col gap-4 p-3">
      <EditorRoot editor={editor}>
        <EditorContent className="rounded-md border p-3" />
        <RevisionHistory
          onSave={(revision) =>
            setRevisions((current) => [
              ...current,
              { id: current.length, value: revision },
            ])
          }
          revisions={revisions}
        />
      </EditorRoot>
    </div>
  );
}
'use client';
 
import type { EditorStateSchemaApi, Value } from 'platejs';
import { compare, type TwoWayComparison } from 'platejs/diff';
import {
  EditorRoot,
  EditorContent,
  useCreateEditor,
  useEditor,
} from 'platejs/react';
import * as React from 'react';
 
import { Button } from '@/components/ui/button';
import { BasicMarksKit } from '@/components/editor/basic-marks';
import { Diff } from '@/components/editor/diff';
 
type Revision =





















































































































































Usage

  • editor.read.value() captures the complete document as one revision.
  • tx.value.replace(revision) restores a saved revision as one undoable update.
  • compare(...) from platejs/diff compares two immutable saved revisions without changing the editor.
  • The copied Diff component shows comparison groups, effects, filters, and navigation.
  • The copied BasicMarksKit provides rich-text marks for the editable document.

Compare saved revisions

The demo captures an initial snapshot. Edit the document and select Save revision to compare the two latest snapshots. Saving another revision starts a new comparison and cancels the obsolete one. The comparison is read-only. To turn a validated comparison into a pending proposal, use proposeAuthoredComparison(...).

Restore a revision

Restore replaces the document with that saved revision in one update:

editor.update((tx) => tx.value.replace(revision));
editor.update((tx) => tx.value.replace(revision));

The saved revisions stay in application state, so restoring one keeps the others.

Persist revisions

Save each editor.read.value() snapshot as one document. To load a revision into a fresh editor, pass it as initialValue:

const saved = editor.read.value();
 
const reloaded = createEditor({
  plugins: BasicMarksKit,
  initialValue: saved,
});
const saved = editor.read.value();
 
const reloaded = createEditor({
  plugins: BasicMarksKit,
  initialValue: saved,
});

The fresh editor starts with empty local undo and redo stacks. If the document has comments, save CommentsJSON through comments.toJSON() with the same revision and load it through initialComments; document undo does not rewind the discussion.

See Authored Changes to propose and review changes instead of replacing the document.

ReturnType
<
ReturnType
<
typeof
useEditor>[
'read'
][
'value'
]>;
type SavedRevision = Readonly<{ id: number; value: Revision }>;
const initialValue: Value = [
{
children: [{ text: 'Save a revision after each edit to compare them.' }],
type: 'paragraph',
},
{
children: [{ text: 'Edit this text, then save a revision.' }],
type: 'paragraph',
},
];
export function VersionDiff({
after,
before,
schema,
}: {
after: unknown;
before: unknown;
schema: EditorStateSchemaApi;
}) {
const [result, setResult] = React.useState<{
after: unknown;
comparison: TwoWayComparison | null;
error: string | null;
} | null>(null);
React.useEffect(() => {
const controller = new AbortController();
void compare({ after, before, schema, signal: controller.signal }).then(
(comparison) => {
if (!controller.signal.aborted) {
setResult({ after, comparison, error: null });
}
},
(error: unknown) => {
if (
!controller.signal.aborted &&
!(error instanceof Error && error.name === 'AbortError')
) {
setResult({
after,
comparison: null,
error:
error instanceof Error ? error.message : 'Comparison failed.',
});
}
}
);
return () => controller.abort();
}, [after, before, schema]);
if (result && result.after === after && result.error) {
return (
<p className="rounded-md border border-red-300 bg-red-50 p-3 text-sm text-red-950">
{result.error}
</p>
);
}
if (!result || result.after !== after || !result.comparison) {
return (
<p aria-live="polite" className="rounded-md border p-3 text-sm">
Comparing revisions…
</p>
);
}
return <Diff comparison={result.comparison} />;
}
function RevisionHistory({
onSave,
revisions,
}: {
onSave: (revision: Revision) => void;
revisions: readonly SavedRevision[];
}) {
const editor = useEditor();
return (
<section className="space-y-2">
<div className="flex items-center justify-between gap-3">
<h2 className="font-medium">Saved revisions</h2>
<Button
onClick={() => onSave(structuredClone(editor.read.value()))}
size="sm"
variant="outline"
>
Save revision
</Button>
</div>
<p className="text-sm text-muted-foreground">
Edit the document and save a revision to compare it with the previous
snapshot, or restore any saved revision.
</p>
<ol className="space-y-1 text-sm">
{revisions.map(({ id, value }, index) => (
<li className="flex items-center justify-between gap-3" key={id}>
<span>Revision {index + 1}</span>
<Button
aria-label={`Restore revision ${index + 1}`}
onClick={() => editor.update((tx) => tx.value.replace(value))}
size="sm"
variant="ghost"
>
Restore
</Button>
</li>
))}
</ol>
{revisions.length > 1 && (
<VersionDiff
after={revisions.at(-1)?.value}
before={revisions.at(-2)?.value}
schema={editor.read.schema}
/>
)}
</section>
);
}
export default function VersionHistoryDemo() {
const editor = useCreateEditor({
plugins: BasicMarksKit,
initialValue,
});
const [revisions, setRevisions] = React.useState<readonly SavedRevision[]>(
() => [{ id: 0, value: structuredClone(editor.read.value()) }]
);
return (
<div className="flex flex-col gap-4 p-3">
<EditorRoot editor={editor}>
<EditorContent className="rounded-md border p-3" />
<RevisionHistory
onSave={(revision) =>
setRevisions((current) => [
...current,
{ id: current.length, value: revision },
])
}
revisions={revisions}
/>
</EditorRoot>
</div>
);
}