Plate
PlateEditorsTemplates
GitHub16kGitHub
DiscordDiscord
  • Feature Kits
  • Plugin
    • Plugin Methods
    • Plugin Shortcuts
    • Plugin Context
    • Plugin Components
    • Plugin Rules
    • Editing Behavior
    • Plugin Input Rules
  • Editor
    • Editor Methods
    • Controlled Value
  • Authored Changes
  • Performance
  • Static Rendering
  • HTML
  • Markdown
  • Form
  • TypeScript
  • Debugging
  • Unit Testing
  • Browser
  • Troubleshooting
  • Locations
  • Transactions
  • Serializing
  • Roots
  • Document Meta
  • Clipboard and Paste
  • Decorations, annotations, and widgets
  • Schema
  • History
  • Pagination
  • Annotations
  • DOM Coverage
  • External Text Views
  • Virtualized Rendering

Controlled Editor Value

PreviousNext

Control initial values, persistence, replacement, and external loading.

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.

Value ownership

Do not control every keystroke

Do not mirror editor.read.children() into React state and pass it back on every change. That fights Plate selection/history and turns normal typing into a full-document replacement loop.

GoalAPI
Set initial content.initialValue in or .
Editor MethodsPerformance

On This Page

Value ownershipSet the initial valuePersist changesReplace or reset contentLoad initial contentInitialize manually
Build your editor
Production-ready AI template and reusable components.
Get all-access
useCreateEditor
createEditor
Persist edits.<EditorRoot onValueChange> or <EditorRoot 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(...).

Set the initial value

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.

components/editor.tsx
import type { Value } from 'platejs';
import { EditorRoot, useCreateEditor } from 'platejs/react';
 
import { Editor, EditorContainer } from '@/components/editor/editor';
 
const initialValue: Value = [
  {
    children: [{ text: 'Initial value' }],
    type: 'paragraph',
  },
];
 
export function MyEditor() {
  const editor = useCreateEditor({
    initialValue,
  });
 
  return (
    <EditorRoot editor={editor}>
      <EditorContainer>
        <Editor />
      </EditorContainer>
    </EditorRoot>
  );
}
components/editor.tsx
import type { Value } from 'platejs';
import { EditorRoot, useCreateEditor } from 'platejs/react';
 
import { Editor, EditorContainer } from '@/components/editor/editor';
 
const initialValue: Value = [
  {
    children: [{ text: 'Initial value' }],
    type: 'paragraph',
  },
];
 
export function MyEditor() {
  const editor = useCreateEditor({
    initialValue,
  });
 
  return (
    <EditorRoot editor={editor}>





Persist changes

Use onValueChange when you only need the document value.

components/editor.tsx
import type { EditorDocumentValue, EditorSchemaIdentity, Value } from 'platejs';
import { EditorRoot, useCreateEditor } 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.

components/editor.tsx
<EditorRoot
  editor={editor}
  onValueChange={({ editor, value }) => {
    console.info(editor.id, value);
  }}
/>
components/editor.tsx
<EditorRoot
  editor={editor}
  onValueChange={({ 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.

components/replace-controls.tsx
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.

Loading…

Load initial content

Load remote content before constructing the editor. The loader owns aborts, stale responses, retries, and errors; the editor receives one synchronous initial document.

components/async-editor.tsx
import * as React from 'react';
import type { Value } from 'platejs';
import { EditorRoot, useCreateEditor } from 'platejs/react';
 
import { Editor, EditorContainer } from '@/components/editor/editor';
 
function LoadedEditor({ initialValue }: { initialValue: Value }) {
  const editor = useCreateEditor({
    initialValue,
  });
 
  return (
    <EditorRoot editor={editor}>
      <EditorContainer>

























Initialize manually

Use skipInitialization when another system owns the startup moment, such as collaboration or a multi-step loader. A complete replacement runs configured schema fitting against current input. Convert historical persisted data with migrateDocument before the replacement transaction.

components/manual-init-editor.tsx
import * as React from 'react';
import { EditorRoot, useCreateEditor } from 'platejs/react';
 
import { Editor, EditorContainer } from '@/components/editor/editor';
 
export function ManualInitEditor() {
  const editor = useCreateEditor({
    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.

<EditorContainer>
<Editor />
</EditorContainer>
</EditorRoot>
);
}
localStorage.
setItem
(
STORAGE_KEY,
JSON.stringify({ document: value, schema })
);
}
export function MyEditor() {
const editor = useCreateEditor({
initialValue: () => {
const saved = localStorage.getItem(STORAGE_KEY);
return saved ? JSON.parse(saved) : initialValue;
},
});
return (
<EditorRoot
editor={editor}
onValueChange={({ editor, value }) =>
saveValue(value, editor.read.schema.identity())
}
>
<EditorContainer>
<Editor />
</EditorContainer>
</EditorRoot>
);
}
components/editor.tsx
import type { EditorDocumentValue, EditorSchemaIdentity, Value } from 'platejs';
import { EditorRoot, useCreateEditor } 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 = useCreateEditor({
    initialValue: () => {
      const saved = localStorage.getItem(STORAGE_KEY);
 
      return saved ? JSON.parse(saved) : initialValue;
    },
  });
 
  return (
    <EditorRoot
      editor={editor}
      onValueChange={({ editor, value }) =>
        saveValue(value, editor.read.schema.identity())
      }
    >
      <EditorContainer>
        <Editor />
      </EditorContainer>
    </EditorRoot>
  );
}
/>
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>
);
}
components/replace-controls.tsx
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>
  );
}
<Editor />
</EditorContainer>
</EditorRoot>
);
}
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} />;
}
components/async-editor.tsx
import * as React from 'react';
import type { Value } from 'platejs';
import { EditorRoot, useCreateEditor } from 'platejs/react';
 
import { Editor, EditorContainer } from '@/components/editor/editor';
 
function LoadedEditor({ initialValue }: { initialValue: Value }) {
  const editor = useCreateEditor({
    initialValue,
  });
 
  return (
    <EditorRoot editor={editor}>
      <EditorContainer>
        <Editor />
      </EditorContainer>
    </EditorRoot>
  );
}
 
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} />;
}
then
((
data
)
=>
{
editor.update((tx) => {
tx.value.replace(data.persistedDocument);
const end = tx.points.end([]);
if (end) tx.selection.set(end);
});
});
}, [editor]);
return (
<EditorRoot editor={editor}>
<EditorContainer>
<Editor />
</EditorContainer>
</EditorRoot>
);
}
components/manual-init-editor.tsx
import * as React from 'react';
import { EditorRoot, useCreateEditor } from 'platejs/react';
 
import { Editor, EditorContainer } from '@/components/editor/editor';
 
export function ManualInitEditor() {
  const editor = useCreateEditor({
    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 (
    <EditorRoot editor={editor}>
      <EditorContainer>
        <Editor />
      </EditorContainer>
    </EditorRoot>
  );
}