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 a Sibling or Ancestor of Plate
If you need to access the editor
outside of the Plate
component, or if you're working with multiple editors and want to access whichever editor is currently active, you can use a PlateController
component. PlateController
is an optional component that can be rendered as an ancestor of one or more Plate
components. For example, you can render a PlateController
at the root of your app.
The PlateController
will keep track of which editor is currently active. By default, the first mounted editor inside the PlateController
will be used as the active editor (this can be disabled for individual editors by passing primary={false}
to the Plate
component). After that, every time an editor is focused, that editor becomes active. An editor remains active until another editor is focused or the active editor unmounts.
Inside a PlateController
, hooks like useEditorRef
and useEditorSelector
will return or operate on the currently active editor. If no editor is active, these hooks may return a fallback editor. This can happen under the following circumstances:
- When no
Plate
component is mounted inside thePlateController
- When all mounted
Plate
components are marked as non-primary - Temporarily while
PlateContent
is in the process of mounting
The fallback editor is designed to ensure that code querying the editor
produces sensible default values, the same as if they had been given an empty editor with no plugins. The fallback editor cannot be mutated, and will throw a runtime error if any state-changing operations are applied to it. To check if your component is accessing a real editor or a fallback editor, use the useEditorMounted
hook (which returns false for a fallback editor), or check editor.isFallback
.
const Toolbar = () => {
const editor = useEditorState(); // Returns the active editor (or a fallback editor)
const isMounted = useEditorMounted(); // Returns false if no editor is mounted
// ...
};
const App = () => {
return (
<PlateController>
<Toolbar />
<Plate>
<PlateContent />
</Plate>
<Plate>
<PlateContent />
</Plate>
</PlateController>
);
};
From the Parent of Plate
The editorRef
prop is an older, alternative way of accessing the editor
outside of Plate
, and works best in cases where the editor
instance must be accessed in the Plate
component's parent. In all other cases, PlateController
is the preferred solution (see above).
The ref can be created with useRef
, the setter function of useState
, or a custom ref callback. It is populated with the editor
instance when Plate
mounts, and null
when Plate
unmounts. Since the editor
is unavailable during the first render, and may become unavailable if the Plate
component unmounts, you must handle the case where editor
is null
.
const App = () => {
const editorRef = useRef<PlateEditor | null>(null);
const handleSomeEvent = useCallback(() => {
const editor = editorRef.current;
// Handle the case where editor is null
if (editor) {
// Do something with editor
}
}, []);
<Plate editorRef={editorRef}>
<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>
);
};