Controlled Editor Value

PreviousNext

Control initial values, persistence, replacement, and async initialization.

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

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

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

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

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

components/replace-controls.tsx
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>
  );
}
components/replace-controls.tsx
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>
  );
}
Loading…

Load Async Initial Content

Use an async value function when the editor can initialize as soon as the data resolves.

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

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