Docs
Accessing the Editor

Accessing the Editor

Getting a reference to the editor instance

To do most things with Plate, you'll need to access the editor instance at one point or another. The way you'll do this depends on the context in which you need to access the editor.

From Inside a Plugin

Most often, when you want to extend the functionality of your editor, you'll create a custom Plate plugin. Luckily, plugins are one of the easiest places to access the editor instance.

Inside Event Handlers

Use the first argument of the handler function.

const createMyPlugin = createPluginFactory({
  key: KEY_MY_PLUGIN,
  handlers: {
    onKeyDown: (editor) => (event) => {
      // Do something with editor
    },
    onChange: (editor) => (value) => {
      // Do something with editor
    },
  },
});

Using the Then Option

The purpose of the then option is to access the editor instance in plugin options that normally don't have access to it. Pass a function that takes an editor and returns an object to be merged with the top-level plugin options.

For example, suppose you had this code and wanted to access the editor instance inside deserializeHtml:

const createMyPlugin = createPluginFactory({
  key: KEY_MY_PLUGIN,
  deserializeHtml: {
    // Need editor here
  },
});

You would wrap the deserializeHtml option inside then.

const createMyPlugin = createPluginFactory({
  key: KEY_MY_PLUGIN,
  then: (editor) => ({
    deserializeHtml: {
      // Do something with editor
    },
  }),
});

From a Child of Plate

Use the useEditorRef, useEditorSelector or useEditorState hooks. Which of these hooks you should use depends on when you want your component to re-render in response to changes to editor.

  • useEditorRef - Use a reference to editor that almost never gets replaced. This should be the default choice.
    • Since editor is a mutable object that gets updated by reference, useEditorRef is always sufficient for accessing the editor inside callbacks.
    • useEditorRef will almost never cause your component to re-render, so it's the best choice for performance.
  • useEditorSelector - Subscribe to a specific selector based on editor. This is the most performant option for subscribing to state changes.
    • Example usage: const hasSelection = useEditorSelector((editor) => !!editor.selection, []);
    • When you want your component to re-render in response to a specific change that you're interested in, you can use useEditorSelector to access the relevant property.
    • The selector function is called every time the editor changes (i.e. on every keystroke or selection change), but the component only re-renders when the return value changes.
      • For this to work properly, you should make sure that the return value can be compared using ===. In most cases, this means returning a primitive value, like a number, string or boolean.
      • You can provide a custom equalityFn in the options to useEditorSelector for cases where === isn't sufficient.
    • If the selector function depends on any locally scoped variables, you should include these in the dependency list.
  • useEditorState - Re-render every time the editor changes.
    • Using useEditorState will cause your component to re-render every time the user presses a key or changes the selection.
    • This may cause performance issues for large documents, or when re-rendering is particularly expensive.

You can call these hooks from any React component that is rendered as a descendant of the Plate component, including Plugin Components.

const Toolbar = () => {
  const boldActive = useEditorSelector((editor) => isMarkActive(editor, MARK_BOLD), []);
  // ...
};
 
const Editor = () => (
  <Plate>
    <Toolbar />
    <PlateContent />
  </Plate>
);
const ParagraphElement = ({
  className,
  children,
  ...props
}: PlateElementProps) => {
  const editor = useEditorRef();
 
  const handleClick = useCallback(() => {
    console.info('You clicked on a paragraph, and the editor is ', editor);
  }, [editor]);
 
  return (
    <PlateElement asChild className={className} {...props}>
      <p onClick={handleClick}>{children}</p>
    </PlateElement>
  );
};

One common pattern is to add an effect component as a child of Plate that consumes editor and returns null.

const CustomEffect = () => {
  const editor = useEditorRef();
 
  useEffect(() => {
    const interval = setInterval(() => {
      console.info('The editor is ', editor);
    }, 1000);
 
    return () => clearInterval(interval);
  }, [editor]);
 
  return null;
};
 
export default () => (
  <Plate>
    <CustomEffect />
 
    <PlateContent />
  </Plate>
);

From an Ancestor

If you need to access the editor instance from an ancestor of PlateContent, wrapping the relevant components in a Plate is the preferred solution. If this is not an option, you can instead use the editorRef prop to pass a reference to the editor instance up the React component tree to where it is needed.

The editorRef prop can be used with useRef, useState, or a custom ref callback. Regardless of which you use, you'll need to handle the case where editor is null. This happens when the editor hasn't had a chance to render yet or has unmounted.

With a Ref Object

const App = () => {
  const editorRef = useRef<PlateEditor | null>(null);
 
  const handleSomeEvent = useCallback(() => {
    // editor has type PlateEditor | null
    const editor = editorRef.current;
 
    if (editor) {
      // Do something with editor
    }
  }, []);
 
  // Pass editorRef and handleSomeEvent down to where they're needed
  // ...
};
 
const Editor = ({
  editorRef,
}: {
  editorRef: MutableRefObject<PlateEditor | null>;
}) => (
  <Plate editorRef={editorRef}>
    <PlateContent />
  </Plate>
);

With State

If you want your ancestor component to re-render when the editor content changes, you may want to use useState to store your editor instance. Since the editorRef callback is only called once when the editor first mounts, you'll also need to manually trigger a re-render by updating a counter whenever the onChange handler of Plate is called.

Using editorRef with useState without a counter is equivalent to using useEditorRef instead of useEditorState (the difference is discussed above). Most of the time, if you don't need the ancestor component to re-render on every change, you should be using useRef instead.

const App = () => {
  const [editor, setEditor] = useState<PlateEditor | null>(null);
  const [, handleUpdateEditor] = useReducer((x) => x + 1, 0);
 
  // Pass editor, setEditor and handleUpdateEditor down to where they're needed
  // ...
};
 
const EditorPreview = ({ editor }: { editor: PlateEditor | null }) => {
  // html has type string | null
  const html = useMemo(
    () =>
      editor &&
      serializeHtml(editor, {
        nodes: editor.children,
      }),
    [editor, editor?.children]
  );
 
  if (!html) return null;
  return <div dangerouslySetInnerHTML={{ __html: html }} />;
};
 
const Editor = ({
  setEditor,
  handleUpdateEditor,
}: {
  setEditor: (editor: PlateEditor | null) => void;
  handleUpdateEditor: () => void;
}) => (
  <Plate editorRef={setEditor} onChange={handleUpdateEditor}>
    <PlateContent />
  </Plate>
);

Temporary Editor Instance

Sometimes, you'll need to access an editor instance, but not necessarily the same editor instance that is used by the Plate editor itself. Such cases include serializing a Plate value to HTML (either on the client or on the server) and deserializing HTML to produce an initial value for Plate.

In these cases, you can create a temporary editor instance using createPlateEditor({ plugins }). The only requirement is that you pass the same set of plugins to createPlateEditor as you pass to the Plate editor itself.

See the following example to deserialize a HTML value and use it as the initial value of the Plate editor.

// Alternatively, define the plugins inside the React component using useMemo
const plugins = createPlugins([
  // ...
]);
 
const Editor = ({ initialHtml }: { initialHtml: string }) => {
  /**
   * Changing the initialValue after render is not supported, so initialHtml
   * is not included in the useMemo deps.
   */
  const initialValue = useMemo(() => {
    const tmpEditor = createPlateEditor({ plugins });
    return deserializeHtml(tmpEditor, {
      element: initialHtml,
    });
  }, []);
 
  return (
    <Plate plugins={plugins} initialValue={initialValue}>
      <PlateContent />
    </Plate>
  );
};