受控编辑器值

如何控制编辑器的值。

在 Plate 中实现完全受控的编辑器值较为复杂,原因如下:

  1. 编辑器状态不仅包含内容 (editor.children),还包含 editor.selectioneditor.history

  2. 直接替换 editor.children 可能会破坏选区(selection)和历史记录(history),导致意外行为或崩溃。

  3. 所有对编辑器值的更改都应通过 Transforms 进行,以保持与选区(selection)和历史记录(history)的一致性。

鉴于这些挑战,通常建议将 Plate 作为非受控输入使用。但如果需要从外部更改编辑器内容,可以使用 editor.tf.setValue(value) 函数。

性能考量

使用 editor.tf.setValue 会在每次调用时重新渲染所有节点,因此应谨慎使用。 如果频繁调用或处理大型文档,可能会影响性能。

或者,您可以使用 editor.tf.reset() 来重置编辑器状态,这将同时重置选区(selection)和历史记录(history)。

异步初始值

您可以使用 skipInitialization 延迟编辑器初始化,直到异步数据准备就绪。然后调用 editor.tf.init 并传入您的值:

function AsyncControlledEditor() {
  const [initialValue, setInitialValue] = React.useState();
  const [loading, setLoading] = React.useState(true);
  const editor = usePlateEditor({
    skipInitialization: true,
  });
 
  React.useEffect(() => {
    // 模拟异步获取
    setTimeout(() => {
      setInitialValue([
        {
          type: 'p',
          children: [{ text: '已加载异步值!' }],
        },
      ]);
      setLoading(false);
    }, 1000);
  }, []);
 
  React.useEffect(() => {
    if (!loading && initialValue) {
      editor.tf.init({ value: initialValue, autoSelect: 'end' });
    }
  }, [loading, initialValue, editor]);
 
  if (loading) return <div>加载中…</div>;
 
  return (
    <Plate editor={editor}>
      <EditorContainer>
        <Editor />
      </EditorContainer>
    </Plate>
  );
}
Files
components/controlled-demo.tsx
'use client';

import * as React from 'react';

import { Plate, usePlateEditor } from 'platejs/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>
  );
}
controlled-demo
controlled-demo