This example installs native authored changes with retainHistory: true. Edit as Alice or Bob, revert an accepted contribution, or save two revisions to inspect their structural comparison.
For application revision previews and restores with comments, see Comments and versions.
'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']>;
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 Revision[];
}) {
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((revision, index) => (
<li
className="flex items-center justify-between gap-3"
// Revisions are append-only, so a position names one revision.
key={index}
>
<span>Revision {index + 1}</span>
<Button
aria-label={`Restore revision ${index + 1}`}
onClick={() => editor.update((tx) => tx.value.replace(revision))}
size="sm"
variant="ghost"
>
Restore
</Button>
</li>
))}
</ol>
{revisions.length > 1 && (
<VersionDiff
after={revisions.at(-1)}
before={revisions.at(-2)}
schema={editor.read.schema}
/>
)}
</section>
);
}
export default function VersionHistoryDemo() {
const editor = useCreateEditor({
plugins: BasicMarksKit,
initialValue,
});
const [revisions, setRevisions] = React.useState<readonly Revision[]>(() => [
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, 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 =
| Piece | Owner | Role |
|---|---|---|
AuthoredPlugin.configure({ initialState: { retainHistory: true } }) | platejs/authored | Captures accepted edits with stable author and change identities and retains their edit bodies. The editor reads the option once, when it is created. |
userId | application | Names the author of each accepted edit; an editor without one writes as the local user. |
read.authored.changes(...) | native authored reads | Returns accepted history as cursor-paged AuthoredChange records. |
update.authored.revert(...) | native authored updates | Creates a compensating contribution for the selected retained change. |
compare(...) | platejs/diff | Compares two immutable saved revisions without changing the editor. |
Diff | copied Plate UI | Shows comparison groups, effects, filters, and navigation. |
BasicMarksKit | copied Plate UI | Provides normal rich-text marks for the editable document. |
authored and retainHistory: true.update.authored.revert(...).A revert can be blocked by dependent work or become unavailable when the required retained content has expired. Local undo and redo remain separate interaction history. Use native decide(...) to accept or reject pending review proposals.
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; an obsolete comparison is cancelled. The comparison is read-only. To turn a validated comparison into a pending proposal, use proposeAuthoredComparison(...).
Save editor.read.value() as one document. The authored envelope includes accepted content, causal records, decisions, and the retained bodies selected by policy.
const saved = editor.read.value();
const reloaded = createEditor({
plugins: [
AuthoredPlugin.configure({ initialState: { retainHistory: true } }),
],
initialValue: saved,
userId: session.user.id,
});const saved = editor.read.value();
const reloaded = createEditor({
plugins: [
AuthoredPlugin.configure({ initialState: { retainHistory: true } }),
],
initialValue: saved,
userId: session.user.id,
});The fresh editor starts with empty local undo and redo stacks. Retained authored contributions remain in the saved document. 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 for review views, decision results, collaboration, and format projections.