Plate is not a normal controlled text input. The editor owns content, selection, history, plugin state, and normalization. This guide shows the safe control points: synchronous initial values, change persistence, explicit replacement, reset, and externally owned loading.
Do not mirror editor.children into React state and pass it back on every
change. That fights Plite selection/history and turns normal typing into a
full-document replacement loop.
| Goal | API |
|---|---|
| Set initial content. | initialValue in or . |
usePlateEditorcreatePlateEditor| Persist edits. | <Plate onValueChange> or <Plate onCommit>. |
| Replace content from outside the editor. | editor.update((tx) => tx.value.replace({ children: value })). |
| Restore initial content. | editor.update((tx) => tx.value.replace({ children: initialValue })). |
| Delay initialization. | skipInitialization: true plus an explicit editor.update(...). |
Pass a Value or a synchronous initializer to initialValue. A synchronous
initializer can read the compiled editor when decoding HTML or another
model-aware format.
import type { Value } from 'platejs';
import { Plate, usePlateEditor } from 'platejs/react';
import { Editor, EditorContainer } from '@/components/editor/editor';
const initialValue: Value = [
{
children: [{ text: 'Initial value' }],
type: 'paragraph',
},
];
export function MyEditor() {
const editor = usePlateEditor({
initialValue,
});
return (
<Plate editor={editor}>
<EditorContainer>
<Editor />
</EditorContainer>
</Plate>
);
}import type { Value } from 'platejs';
import { Plate, usePlateEditor } from 'platejs/react';
import { Editor, EditorContainer } from '@/components/editor/editor';
const initialValue: Value = [
{
children: [{ text: 'Initial value' }],
type: 'paragraph',
},
];
export function MyEditor() {
const editor = usePlateEditor({
initialValue,
});
return (
<Plate editor={editor}>
Use onValueChange when you only need the document value.
import type { EditorDocumentValue, EditorSchemaIdentity, Value } from 'platejs';
import { Plate, usePlateEditor } from 'platejs/react';
import { Editor, EditorContainer } from '@/components/editor/editor';
const STORAGE_KEY = 'plate-value';
const initialValue: Value = [
{
children: [{ text: 'Autosaved value' }],
type: 'paragraph',
},
];
function saveValue(value: EditorDocumentValue, schema: EditorSchemaIdentity) {
onValueChange receives the canonical commit context and the complete
serializable document. Durable storage should persist { document, schema }
so primary children, named roots, persisted meta, and source schema
identity stay together.
<Plate
editor={editor}
onValueChange={({ editor, value }) => {
console.info(editor.id, value);
}}
/><Plate
editor={editor}
onValueChange={({ editor, value }) => {
console.info(editor.id, value);
}}
Use a transaction group for external changes. tx.value.replace(...) replaces
the document with an explicit value.
import type { Value } from 'platejs';
import { useEditor } from 'platejs/react';
import { Button } from '@/components/ui/button';
const initialValue: Value = [
{
children: [{ text: 'Initial value' }],
type: 'paragraph',
},
];
const replacementValue: Value = [
{
children: [{ text: 'Replaced value' }],
type: 'paragraph',
},
];
tx.value.replace(...) replaces the complete serializable document. A
children-only input removes named roots and resets persisted meta. Use it for
explicit outside-editor changes, not for every onValueChange.
Load remote content before constructing the editor. The loader owns aborts, stale responses, retries, and errors; the editor receives one synchronous initial document.
import * as React from 'react';
import type { Value } from 'platejs';
import { Plate, usePlateEditor } from 'platejs/react';
import { Editor, EditorContainer } from '@/components/editor/editor';
function LoadedEditor({ initialValue }: { initialValue: Value }) {
const editor = usePlateEditor({
initialValue,
});
return (
<Plate editor={editor}>
<EditorContainer>
Use skipInitialization when another system owns the startup moment, such as
collaboration or a multi-step loader. A complete replacement runs configured
version steps, then each installed plugin's prepareDocument, then schema
fitting. Plugins use preparation for current-schema invariants, not historical
migrations.
import * as React from 'react';
import { Plate, usePlateEditor } from 'platejs/react';
import { Editor, EditorContainer } from '@/components/editor/editor';
export function ManualInitEditor() {
const editor = usePlateEditor({
skipInitialization: true,
});
React.useEffect(() => {
void fetch('/api/document')
.then((response) => response.json())
.
Done. Plate owns live editor state; your app controls the entry points around it.
import type { EditorDocumentValue, EditorSchemaIdentity, Value } from 'platejs';
import { Plate, usePlateEditor } from 'platejs/react';
import { Editor, EditorContainer } from '@/components/editor/editor';
const STORAGE_KEY = 'plate-value';
const initialValue: Value = [
{
children: [{ text: 'Autosaved value' }],
type: 'paragraph',
},
];
function saveValue(value: EditorDocumentValue, schema: EditorSchemaIdentity) {
localStorage.setItem(
STORAGE_KEY,
JSON.stringify({ document: value, schema })
);
}
export function MyEditor() {
const editor = usePlateEditor({
initialValue: () => {
const saved = localStorage.getItem(STORAGE_KEY);
return saved ? JSON.parse(saved) : initialValue;
},
});
return (
<Plate
editor={editor}
onValueChange={({ editor, value }) =>
saveValue(value, editor.read.schema.identity())
}
>
<EditorContainer>
<Editor />
</EditorContainer>
</Plate>
);
}import type { Value } from 'platejs';
import { useEditor } from 'platejs/react';
import { Button } from '@/components/ui/button';
const initialValue: Value = [
{
children: [{ text: 'Initial value' }],
type: 'paragraph',
},
];
const replacementValue: Value = [
{
children: [{ text: 'Replaced value' }],
type: 'paragraph',
},
];
export function ReplaceControls() {
const editor = useEditor();
return (
<div className="flex gap-2">
<Button
onClick={() => {
editor.update((tx) => {
tx.value.replace({ children: replacementValue });
});
}}
>
Replace Value
</Button>
<Button
onClick={() => {
editor.update((tx) => {
tx.value.replace({ children: initialValue });
});
}}
>
Reset Editor
</Button>
</div>
);
}import * as React from 'react';
import type { Value } from 'platejs';
import { Plate, usePlateEditor } from 'platejs/react';
import { Editor, EditorContainer } from '@/components/editor/editor';
function LoadedEditor({ initialValue }: { initialValue: Value }) {
const editor = usePlateEditor({
initialValue,
});
return (
<Plate editor={editor}>
<EditorContainer>
<Editor />
</EditorContainer>
</Plate>
);
}
export function AsyncEditor() {
const [initialValue, setInitialValue] = React.useState<Value | null>(null);
React.useEffect(() => {
const controller = new AbortController();
void fetch('/api/document', { signal: controller.signal })
.then((response) => response.json())
.then((data) => setInitialValue(data.content))
.catch((error) => {
if (error.name !== 'AbortError') throw error;
});
return () => controller.abort();
}, []);
if (!initialValue) return <p>Loading editor…</p>;
return <LoadedEditor initialValue={initialValue} />;
}import * as React from 'react';
import { Plate, usePlateEditor } from 'platejs/react';
import { Editor, EditorContainer } from '@/components/editor/editor';
export function ManualInitEditor() {
const editor = usePlateEditor({
skipInitialization: true,
});
React.useEffect(() => {
void fetch('/api/document')
.then((response) => response.json())
.then((data) => {
editor.update((tx) => {
tx.value.replace(data.persistedDocument);
const end = tx.points.end([]);
if (end) tx.selection.set(end);
});
});
}, [editor]);
return (
<Plate editor={editor}>
<EditorContainer>
<Editor />
</EditorContainer>
</Plate>
);
}