From zbeyens. The source code is available on GitHub.

Plate
PlatePliteEditorsTemplates
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
  • Performance
  • Static Rendering
  • HTML
  • Markdown
  • Form
  • TypeScript
  • Debugging
  • Unit Testing
  • Browser
  • Troubleshooting

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.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.

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
usePlateEditor
createPlateEditor
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(...).

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 { 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>
  );
}
components/editor.tsx
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}>





Persist Changes

Use onValueChange when you only need the document value.

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

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

























Initialize Manually

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.

components/manual-init-editor.tsx
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.

<
EditorContainer
>
<Editor />
</EditorContainer>
</Plate>
);
}
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>
);
}
components/editor.tsx
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>
  );
}
/>
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>
</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} />;
}
components/async-editor.tsx
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} />;
}
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>
);
}
components/manual-init-editor.tsx
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>
  );
}