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
or useEditorState
hooks.
Internally, useEditorState
is a wrapper for useEditorRef
. The only difference is that useEditorState
causes React to re-render whenever the editor
state changes, whereas useEditorRef
does not cause a re-render. Since editor
is mutable and is updated by reference, useEditorRef
will be sufficient (and more efficient) in most situations.
You can call these hooks from any React component that is rendered as a descendant of the Plate
component, including Plugin Components.
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 of Plate
Wrap PlateContent
and the sibling in Plate
, and then use useEditorRef
or useEditorState
from within the sibling.
const Toolbar = () => {
const editor = useEditorState();
// Do something with editor
// ...
};
const Editor = () => (
<Plate>
<Toolbar />
<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>
);
};