Back to Plate

Controlled Editor Value

content/docs/(guides)/controlled.mdx

53.0.85.3 KB
Original Source

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

<Callout type="warning" title="Do not control every keystroke"> Do not mirror `editor.children` into React state and pass it back on every change. That fights Slate selection/history and turns normal typing into a full-document replacement loop. </Callout>
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(...).
<Steps>

Set the Initial Value

Pass a Value, an HTML string, a function, or an async function to value.

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.

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.

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.

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>
  );
}
<Callout type="info"> `editor.tf.setValue` replaces nodes at the document root. Use it for explicit outside-editor changes, not for every `onValueChange`. </Callout> <ComponentPreview name="controlled-demo" padding="md" />

Load Async Initial Content

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

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.

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>
  );
}
</Steps>

Done. Plate owns live editor state; your app controls the entry points around it.