content/docs/(guides)/controlled.mdx
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.
| Goal | API |
|---|---|
| Set initial content. | value in usePlateEditor or createPlateEditor. |
| Persist edits. | <Plate onValueChange> or <Plate onChange>. |
| Replace content from outside the editor. | editor.tf.setValue(value). |
| Reset editor state. | editor.tf.reset(). |
| Delay initialization. | skipInitialization: true plus editor.tf.init(...). |
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>
);
}
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>
);
}
Use onChange when the callback needs the editor instance too.
<Plate
editor={editor}
onChange={({ editor, value }) => {
console.info(editor.id, value);
}}
/>
Use transforms for external changes. setValue replaces the document and
reset returns the editor to its initialized state.
import type { Value } from 'platejs';
import { useEditorRef } from 'platejs/react';
import { Button } from '@/components/ui/button';
const replacementValue: Value = [
{
children: [{ text: 'Replaced value' }],
type: 'p',
},
];
export function ReplaceControls() {
const editor = useEditorRef();
return (
<div className="flex gap-2">
<Button onClick={() => editor.tf.setValue(replacementValue)}>
Replace Value
</Button>
<Button onClick={() => editor.tf.reset()}>Reset Editor</Button>
</div>
);
}
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>
);
}
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.tf.init({
autoSelect: 'end',
value: data.content,
});
});
}, [editor]);
return (
<Plate editor={editor}>
<EditorContainer>
<Editor />
</EditorContainer>
</Plate>
);
}
Done. Plate owns live editor state; your app controls the entry points around it.