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.tf.setValue(value).
Reset editor state.editor.tf.reset().
Delay initialization.skipInitialization: true plus editor.tf.init(...).

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 transforms for external changes. setValue replaces the document and reset returns the editor to its initialized state.

components/replace-controls.tsx
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>
  );
}
components/replace-controls.tsx
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>
  );
}
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.tf.init({
          autoSelect: 'end',
          value: data.content,
        });
      });
  }, [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.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.