Command Palette

Search for a command to run...

Controlled Editor Value

How to control the editor value.

Implementing a fully controlled editor value in Plate (and Slate) is complex due to several factors:

  1. The editor state includes more than just the content (editor.children). It also includes editor.selection and editor.history.

  2. Directly replacing editor.children can break the selection and history, leading to unexpected behavior or crashes.

  3. All changes to the editor's value should ideally happen through Transforms to maintain consistency with selection and history.

Given these challenges, it's generally recommended to use Plate as an uncontrolled input. However, if you need to make external changes to the editor's content, you can use editor.tf.setValue(value) function.

Performance Consideration

Using editor.tf.setValue will re-render all nodes on each call, so it should be used carefully and sparingly. It may impact performance if used frequently or with large documents.

Alternatively, you can use editor.tf.reset() to reset the editor state, which will reset the selection and history.

Async Initial Value

You can use skipInitialization to delay editor initialization until your async data is ready. Then, call editor.tf.init with your value:

function AsyncControlledEditor() {
  const [initialValue, setInitialValue] = React.useState();
  const [loading, setLoading] = React.useState(true);
  const editor = usePlateEditor({
    skipInitialization: true,
  });
 
  React.useEffect(() => {
    // Simulate async fetch
    setTimeout(() => {
      setInitialValue([
        {
          type: 'p',
          children: [{ text: 'Loaded async value!' }],
        },
      ]);
      setLoading(false);
    }, 1000);
  }, []);
 
  React.useEffect(() => {
    if (!loading && initialValue) {
      editor.tf.init({ value: initialValue, autoSelect: 'end' });
    }
  }, [loading, initialValue, editor]);
 
  if (loading) return <div>Loading…</div>;
 
  return (
    <Plate editor={editor}>
      <EditorContainer>
        <Editor />
      </EditorContainer>
    </Plate>
  );
}
Loading...
Files
components/controlled-demo.tsx
'use client';

import * as React from 'react';

import { Plate, usePlateEditor } from '@udecode/plate/react';

import { Button } from '@/components/ui/button';
import { Editor, EditorContainer } from '@/components/ui/editor';

export default function ControlledEditorDemo() {
  const editor = usePlateEditor({
    value: [
      {
        children: [{ text: 'Initial Value' }],
        type: 'p',
      },
    ],
  });

  return (
    <div>
      <Plate editor={editor}>
        <EditorContainer>
          <Editor className="px-0" />
        </EditorContainer>
      </Plate>

      <div className="mt-4 flex flex-col gap-2">
        <Button
          onClick={() => {
            // Replace with HTML string
            editor.tf.setValue([
              {
                children: [{ text: 'Replaced Value' }],
                type: 'p',
              },
            ]);

            editor.tf.focus({ edge: 'endEditor' });
          }}
        >
          Replace Value
        </Button>

        <Button
          onClick={() => {
            editor.tf.reset();
            editor.tf.focus();
          }}
        >
          Reset Editor
        </Button>
      </div>

      <hr className="my-8" />
      <h2 className="mb-2 text-lg font-semibold">Async Controlled Editor</h2>
      <AsyncControlledEditorDemo />
    </div>
  );
}

function AsyncControlledEditorDemo() {
  const [initialValue, setInitialValue] = React.useState<
    { children: { text: string }[]; type: string }[] | undefined
  >(undefined);
  const [loading, setLoading] = React.useState(true);
  const editor = usePlateEditor({
    skipInitialization: true,
  });

  React.useEffect(() => {
    // Simulate async fetch
    setTimeout(() => {
      setInitialValue([
        {
          children: [{ text: 'Loaded async value!' }],
          type: 'p',
        },
      ]);
      setLoading(false);
    }, 1000);
  }, []);

  React.useEffect(() => {
    if (!loading && initialValue) {
      editor.tf.init({ autoSelect: 'end', value: initialValue });
    }
  }, [loading, initialValue, editor]);

  if (loading) return <div>Loading…</div>;

  return (
    <Plate editor={editor}>
      <EditorContainer>
        <Editor className="px-0" />
      </EditorContainer>
    </Plate>
  );
}