Implementing a fully controlled editor value in Plate is complex due to several factors:
-
The editor state includes more than just the content (
editor.children
). It also includeseditor.selection
andeditor.history
. -
Directly replacing
editor.children
can break the selection and history, leading to unexpected behavior or crashes. -
All changes to the editor's value should ideally happen through Transforms to maintain consistency with selection and history.
Given these challenges, it's generally recommended to use Plate as an uncontrolled input. However, if you need to make external changes to the editor's content, you can use editor.tf.setValue(value)
function.
Performance Consideration
Using editor.tf.setValue
will re-render all nodes on each call, so it
should be used carefully and sparingly. It may impact performance if used
frequently or with large documents.
Alternatively, you can use editor.tf.reset()
to reset the editor state, which will reset the selection and history.
Async Initial Value
You can pass an async function directly to the value
option. The editor will handle the async loading automatically:
function AsyncControlledEditor() {
const editor = usePlateEditor({
value: async () => {
// Simulate fetching data from an API
const response = await fetch('/api/document');
const data = await response.json();
return data.content;
},
autoSelect: 'end',
});
return (
<Plate editor={editor}>
<EditorContainer>
<Editor />
</EditorContainer>
</Plate>
);
}
For more control over initialization timing, you can use shouldInitialize: false
and manually call editor.tf.init
:
function AsyncControlledEditor() {
const [data, setData] = React.useState(null);
const editor = usePlateEditor({
shouldInitialize: false,
});
React.useEffect(() => {
const loadData = async () => {
const response = await fetch('/api/document');
const data = await response.json();
setData(data);
editor.tf.init({
value: data.content,
autoSelect: 'end',
onReady: ({ editor, value }) => {
console.info('Editor ready with value:', value);
},
});
};
loadData();
}, [editor]);
if (!data) return <div>Loading…</div>;
return (
<Plate editor={editor}>
<EditorContainer>
<Editor />
</EditorContainer>
</Plate>
);
}
'use client';
import * as React from 'react';
import { Plate, usePlateEditor } from 'platejs/react';
import { Button } from '@/components/ui/button';
import { Editor, EditorContainer } from '@/components/ui/editor';
export default function ControlledEditorDemo() {
const editor = usePlateEditor({
value: [
{
children: [{ text: 'Initial Value' }],
type: 'p',
},
],
});
return (
<div>
<Plate editor={editor}>
<EditorContainer>
<Editor className="px-0" />
</EditorContainer>
</Plate>
<div className="mt-4 flex flex-col gap-2">
<Button
onClick={() => {
// Replace with HTML string
editor.tf.setValue([
{
children: [{ text: 'Replaced Value' }],
type: 'p',
},
]);
editor.tf.focus({ edge: 'endEditor' });
}}
>
Replace Value
</Button>
<Button
onClick={() => {
editor.tf.reset();
editor.tf.focus();
}}
>
Reset Editor
</Button>
</div>
<hr className="my-8" />
<h2 className="mb-2 text-lg font-semibold">Async Controlled Editor</h2>
<AsyncControlledEditorDemo />
</div>
);
}
function AsyncControlledEditorDemo() {
const [initialValue, setInitialValue] = React.useState<
{ children: { text: string }[]; type: string }[] | undefined
>(undefined);
const [loading, setLoading] = React.useState(true);
const editor = usePlateEditor({
skipInitialization: true,
});
React.useEffect(() => {
// Simulate async fetch
setTimeout(() => {
setInitialValue([
{
children: [{ text: 'Loaded async value!' }],
type: 'p',
},
]);
setLoading(false);
}, 1000);
}, []);
React.useEffect(() => {
if (!loading && initialValue) {
editor.tf.init({ autoSelect: 'end', value: initialValue });
}
}, [loading, initialValue, editor]);
if (loading) return <div>Loading…</div>;
return (
<Plate editor={editor}>
<EditorContainer>
<Editor className="px-0" />
</EditorContainer>
</Plate>
);
}
On This Page
Async Initial Value