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 toeditor
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 theeditor
inside callbacks. useEditorRef
will almost never cause your component to re-render, so it's the best choice for performance.
- Since
useEditorSelector
- Subscribe to a specific selector based oneditor
. 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 touseEditorSelector
for cases where===
isn't sufficient.
- For this to work properly, you should make sure that the return value can be compared using
- If the selector function depends on any locally scoped variables, you should include these in the dependency list.
- Example usage:
useEditorState
- Re-render every time theeditor
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.
- Using
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>
);
If editor
is modified by reference, why include it in dependency
lists?
Good question! Even though editor
is usually modified by reference,
there are some situations in which it's replaced with a fresh instance, such
as when the editor is reset.
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>
);
};