This is a simple method I’ve been using to integrate ProseMirror into the standard React rendering lifecycle. It allows plugins, node views, commands etc to access the latest values of any props passed to the Editor
component.
import { schema } from "prosemirror-schema-basic";
import { EditorState, Plugin, PluginKey } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import React, { useEffect, useRef } from "react";
const reactPropsKey = new PluginKey("reactProps");
function reactProps(initialProps) {
return new Plugin({
key: reactPropsKey,
state: {
init: () => initialProps,
apply: (tr, prev) => tr.getMeta(reactPropsKey) || prev,
},
});
}
function Editor(props) {
const viewHost = useRef();
const view = useRef(null);
useEffect(() => { // initial render
const state = EditorState.create({ schema, plugins: [reactProps(props)] });
view.current = new EditorView(viewHost.current, { state });
return () => view.current.destroy();
}, []);
useEffect(() => { // every render
const tr = view.current.state.tr.setMeta(reactPropsKey, props);
view.current.dispatch(tr);
});
return <div ref={viewHost} />;
}
Data flow:
- React re-renders the component when its props change
- This triggers the 2nd
useEffect
, which dispatches a ProseMirror transaction with the new props attached - The
reactProps
plugin sees this transaction and updates its state with the new props - Other parts of the editor can now access these props with
reactPropsKey.getState(state)
As well as working for values, this works for callbacks. E.g. a save command:
keymap({
"Mod-s": (state, dispatch) => {
const { onSave } = reactPropsKey.getState(state);
onSave(state.doc.toJSON());
return true;
},
});
Would invoke a callback passed like this:
<Editor onSave={saveHandler} />
As a bonus the props show up nicely in the plugins tab of prosemirror-dev-tools.
For complex integrations, I think there is still value in lifting the state out of ProseMirror but so far this approach has worked well for me. I hope others find it useful!