Lightweight React integration example

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:

  1. React re-renders the component when its props change
  2. This triggers the 2nd useEffect, which dispatches a ProseMirror transaction with the new props attached
  3. The reactProps plugin sees this transaction and updates its state with the new props
  4. 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!

9 Likes

I really like this approach. It’s really clean.

Have you thought about passing React components as nodes now that they could access state easily?

1 Like

@dharries Thanks for this example!

Though I struggle with understanding how to properly keep state, could you help please?

So I need autosave functionality, and the most straightforward way of doing so is to keep Autosave component and editorState one level up and pass setEditorState function to ProseMirror to execute it everytime state changes. However with this approach I kind a run into infinite loops with useEffect, well because with external state Prosemirror rerenders every time it saved.

So I am trying to refactor it somehow and I run out of ideas.

1 Like

This is really elegant. I hope you don’t mind if I borrow some ideas to use in remirror.

2 Likes

Hey! I am using prosemirror-suggest with this approach. It doesn’t work well. onChange gets called for any change is state… What is the best workaround it?

Thanks to both the author of this post & author of remirror for coming up with such elegant ideas.

Hey, I am also running into infinite loop problem. Did u get any ideas to solve it?

Thanks

This idea is very good. I have always been using an event bus to synchronize React data and ProseMirror data. I tried writing a demo using the approach you provided and it feels like it can replace my event bus method.