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: initial values, change persistence, explicit replacement, reset, and delayed initialization.
Value Ownership
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. | value in usePlateEditor or createPlateEditor. |
| Persist edits. | <Plate onValueChange> or <Plate onChange>. |
| 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(...). |
Set the Initial Value
Pass a Value, an HTML string, a function, or an async function to value.
import type { Value } from 'platejs';
import { Plate, usePlateEditor } from 'platejs/react';
import { Editor, EditorContainer } from '@/components/ui/editor';
const initialValue: Value = [
{
children: [{ text: 'Initial value' }],
type: 'p',
},
];
export function MyEditor() {
const editor = usePlateEditor({
value: 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/ui/editor';
const initialValue: Value = [
{
children: [{ text: 'Initial value' }],
type: 'p',
},
];
export function MyEditor() {
const editor = usePlateEditor({
value: initialValue,
});
return (
<Plate editor={editor}>
<EditorContainer>
<Editor />
</EditorContainer>
</Plate>
);
}Persist Changes
Use onValueChange when you only need the document value.
import type { Value } from 'platejs';
import { Plate, usePlateEditor } from 'platejs/react';
import { Editor, EditorContainer } from '@/components/ui/editor';
const STORAGE_KEY = 'plate-value';
const initialValue: Value = [
{
children: [{ text: 'Autosaved value' }],
type: 'p',
},
];
function saveValue(value: Value) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(value));
}
export function MyEditor() {
const editor = usePlateEditor({
value: () => {
const saved = localStorage.getItem(STORAGE_KEY);
return saved ? JSON.parse(saved) : initialValue;
},
});
return (
<Plate editor={editor} onValueChange={({ value }) => saveValue(value)}>
<EditorContainer>
<Editor />
</EditorContainer>
</Plate>
);
}import type { Value } from 'platejs';
import { Plate, usePlateEditor } from 'platejs/react';
import { Editor, EditorContainer } from '@/components/ui/editor';
const STORAGE_KEY = 'plate-value';
const initialValue: Value = [
{
children: [{ text: 'Autosaved value' }],
type: 'p',
},
];
function saveValue(value: Value) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(value));
}
export function MyEditor() {
const editor = usePlateEditor({
value: () => {
const saved = localStorage.getItem(STORAGE_KEY);
return saved ? JSON.parse(saved) : initialValue;
},
});
return (
<Plate editor={editor} onValueChange={({ value }) => saveValue(value)}>
<EditorContainer>
<Editor />
</EditorContainer>
</Plate>
);
}Use onChange when the callback needs the editor instance too.
<Plate
editor={editor}
onChange={({ editor, value }) => {
console.info(editor.id, value);
}}
/><Plate
editor={editor}
onChange={({ editor, value }) => {
console.info(editor.id, value);
}}
/>Replace or Reset Content
Use a transaction group for external changes. tx.value.replace(...) replaces
the document with an explicit value.
import type { Value } from 'platejs';
import { useEditorRef } from 'platejs/react';
import { Button } from '@/components/ui/button';
const initialValue: Value = [
{
children: [{ text: 'Initial value' }],
type: 'p',
},
];
const replacementValue: Value = [
{
children: [{ text: 'Replaced value' }],
type: 'p',
},
];
export function ReplaceControls() {
const editor = useEditorRef();
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 type { Value } from 'platejs';
import { useEditorRef } from 'platejs/react';
import { Button } from '@/components/ui/button';
const initialValue: Value = [
{
children: [{ text: 'Initial value' }],
type: 'p',
},
];
const replacementValue: Value = [
{
children: [{ text: 'Replaced value' }],
type: 'p',
},
];
export function ReplaceControls() {
const editor = useEditorRef();
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>
);
}tx.value.replace(...) replaces nodes at the document root. Use it for
explicit outside-editor changes, not for every onValueChange.
Load Async Initial Content
Use an async value function when the editor can initialize as soon as the data
resolves.
import { Plate, usePlateEditor } from 'platejs/react';
import { Editor, EditorContainer } from '@/components/ui/editor';
export function AsyncEditor() {
const editor = usePlateEditor({
autoSelect: 'end',
value: async () => {
const response = await fetch('/api/document');
const data = await response.json();
return data.content;
},
onReady: ({ isAsync, value }) => {
if (isAsync) console.info('Loaded value:', value);
},
});
return (
<Plate editor={editor}>
<EditorContainer>
<Editor />
</EditorContainer>
</Plate>
);
}import { Plate, usePlateEditor } from 'platejs/react';
import { Editor, EditorContainer } from '@/components/ui/editor';
export function AsyncEditor() {
const editor = usePlateEditor({
autoSelect: 'end',
value: async () => {
const response = await fetch('/api/document');
const data = await response.json();
return data.content;
},
onReady: ({ isAsync, value }) => {
if (isAsync) console.info('Loaded value:', value);
},
});
return (
<Plate editor={editor}>
<EditorContainer>
<Editor />
</EditorContainer>
</Plate>
);
}Initialize Manually
Use skipInitialization when another system owns the startup moment, such as
collaboration or a multi-step loader.
import * as React from 'react';
import { Plate, usePlateEditor } from 'platejs/react';
import { Editor, EditorContainer } from '@/components/ui/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({ children: data.content });
const end = tx.points.end([]);
if (end) tx.selection.set(end);
});
});
}, [editor]);
return (
<Plate editor={editor}>
<EditorContainer>
<Editor />
</EditorContainer>
</Plate>
);
}import * as React from 'react';
import { Plate, usePlateEditor } from 'platejs/react';
import { Editor, EditorContainer } from '@/components/ui/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({ children: data.content });
const end = tx.points.end([]);
if (end) tx.selection.set(end);
});
});
}, [editor]);
return (
<Plate editor={editor}>
<EditorContainer>
<Editor />
</EditorContainer>
</Plate>
);
}Done. Plate owns live editor state; your app controls the entry points around it.