Using with React

Hey @thelionthatiam - I’m also about to dive into this, did you make any meaningful progress?

I’ve got some React components wrapping a ProseMirror implementation right now (mostly derived from example repos in this forum), but I haven’t done any work making React components for the blocks themselves.

Might be worth looking at how the tiptap folks wrapped up Vue components. From my perspective it’s a pretty slick integration and allows leveraging Vue which has a lot of similarities to React.

Oh hey, that’s a good point. Thanks!

@thelionthatiam / @andreypopp

I just wanted to follow-up here and leave a trail of breadcrumbs for you. I did get ProseMirror and React blocks integrated this week, with some help from @saranrapjs

Here’s a gist demonstration of what I’m doing. It’s a workaround but it seems to hold up.

One thing I want to mention here, which isn’t shown in the above sample, is that cleanup needs to happen whenever these NodeView + React integrations are created. I haven’t had time to do it myself (this is a side project for me), but I believe it would usually look like a method on the NodeView which looks like this:

public destroy = () => {
  ReactDOM.unmountComponentAtNode(this.dom);
}

That should make sure you don’t spawn a bunch of React subtrees and leave them orphaned.

5 Likes

Is this standard JavaScript? I’m familiar with standard JavaScript and TypeScript. Alas, I don’t know what ?someVal means, nor () => *…

I’ve recently been working on a general project called remirror combining react and prosemirror. It’s available here in case you want to follow the progress.

It was initially intended to be a simple port from tiptap but has taken a life of its own and is now quickly becoming quite different.

Like tiptap it’s built on the concept of Extensions, MarkExtensions, and NodeExtensions which add functionality to the underlying prosemirror editor.

It heavily borrows NodeViews from Atlassian for injecting custom react components into the prosemirror editor as part of the plugin state.

Unlike both these editors it’s also built for injection into native mobile context via Expo, React Native and in the future flutter.

It’s still early days but I’ve been able to create some simple examples like:

Twitter UI

Epic Mode

4 Likes

@ifi, remirror looks really great! I really appreciate the fastidious and professional organization of the codebase. Providing a react-friendly layer on top of prosemirror that aims to be just as powerful is an ambitious project and it looks like you are delivering on the potential.

Previously I experimented with atlaskit - their extensive set of prosemirror plugins and integration with react was very interesting - but they don’t make it easy to decouple those components from their cloud provider apis (by design, I’m sure), and the ambiguity around their licenses and the massive scope of their codebase made me feel too anxious to continue exploring atlaskit. Looking forward to using remirror instead, and perhaps contributing to its success along the way.

2 Likes

@100ideas thanks so much for the feedback. There are a number of useful changes coming at some point this week, time permitting. The main change is what I believe to be a significantly simpler API for creating editors with React. You can see the proposal here.

I believe the main thing going forward will be getting the documentation to a level where anybody can jump right in.

I was hesitant to do that at the beginning since the code structure wasn’t quite settled and my understanding of the best way to build the project was constantly changing. Hopefully, now that the API is settling down, I’ll be able to dedicate more time to documentation and onboarding.

Sounds good!

I took a small step into the codebase last night by trying to get the simplest example in the docs working in my app’s react-styleguidist component library.

Copy-pastying the example code basically worked. Here’s a gist of the little editor component I was playing around with (it should be possible to put package.json in root and everything else in src then hit yarn; yarn run styles to get it to run locally).

One area I had to puzzle through was how to properly initialize the <RenderTree /> json viewer. I initially copied-and-pasted the output of the RenderTree component as json back into my source code so I could initialize the editor with more content. However, RenderTree didn’t know how to interpret bold or italic marks that the stock remirror editor generated. I eventually figured out how to pass in a modifed markMap. I didn’t try to hard to read all the documentation since I assumed it was in flux, so I may have missed the details about that. otherwise that info would be good to add to the docs.

I’d be happy to help with that.

Then I got stuck - I want to add a way for the user to to toggle the <EditorLayout> and/or <RenderTree> between menubar edit mode & direct markdown entry mode, a la https://prosemirror.net/examples/markdown/.

As a first step I just wanted to show/hide various components by triple-clicking the RenderTree or executing a special keystroke.

I saw that there are type definitions for prosemirror config props to do that sort of thing in remirror/support/types/prosemirror-view/index.d.ts#L406, but I couldn’t figure out where or how to pass in props to <RenderTree> - could you give me a hint or two?

// remirror/blob/master/support/types/prosemirror-view/index.d.ts#L406
/**
 * Props are configuration values that can be passed to an editor view
 * or included in a plugin. This interface lists the supported props.
 *
 * The various event-handling functions may all return `true` to
 * indicate that they handled the given event. The view will then take
 * care to call `preventDefault` on the event, except with
 * `handleDOMEvents`, where the handler itself is responsible for that.
 *
 * How a prop is resolved depends on the prop. Handler functions are
 * called one at a time, starting with the base props and then
 * searching through the plugins (in order of appearance) until one of
 * them returns true. For some props, the first plugin that yields a
 * value gets precedence.
 */
export interface EditorProps<S extends Schema = any> {
  /**
   * Can be an object mapping DOM event type names to functions that
   * handle them. Such functions will be called before any handling
   * ProseMirror does of events fired on the editable DOM element.
   * Contrary to the other event handling props, when returning true
   * from such a function, you are responsible for calling
   * `preventDefault` yourself (or not, if you want to allow the
   * default behavior).
   */

// skipping down a few lines...

   /**
   * Called when the editor is triple-clicked, after `handleTripleClickOn`.
   */
handleTripleClick?: ((view: EditorView<S>, pos: number, event: MouseEvent) => boolean) | null;

p.s. happy to move this convo over to your repo moving forward, just let me know.

@andreypopp Any updates on that?

@Storyteller So I was just randomly looking up what’s new here and I came past this post and your question. A while ago I had this same issue/goal of trying to implement my nodeviews using React. I looked up the current implementations and made up my own way here https://github.com/TeemuKoivisto/prosemirror-react-typescript-example

It’s not a complete setup that would be easily extendable with nice interfaces but it’s a start and I think you can at least use as a basis. There were some improvements I have on my other repo that I haven’t added yet since it was kinda muddy how I would add the React components with the plugins and state using Mobx in a nice way. I had a plan to make a general toolbox with all the required plumbing included but I got stuck with some other ProseMirror related problems (custom inline nodes are a pain in the ass).

Oh yeah and I should refactor it to use hooks. Hmm. Oh well. Quite busy right now with other stuff so will see when I have time to work on it.

2 Likes

Hey guys - I have been trying to render React components as NodeView too. I have created this repo to show what I am currently doing:

  1. Use a NodeView to render the component
  2. Instead of using ReactDOM.render, I am using ReactDOM.createPortal. I return the portal component to my editor component which then renders it on the page for it to access my app’s context
  3. I’ve wrapped the NodeView in a React context so that its props are available within the component using hooks

Is this the correct way to do it?

5 Likes

I like how you’ve done this a lot.

Remirror is going through an extensive rewrite and I’d love to borrow this. Maybe we could work together on it.

Some of the things I’m thinking through right now

  • React native support
  • Improved API
  • Completely extensible
  • Chainable commands
  • Better tests

I like how elegant your react node view implementation is. You’re definitely on the right track.

4 Likes

Thanks for this example, I did a proof of concept implementation based on this — https://gist.github.com/88ec11a788110a27f6a5ee0068c07f0e

Hey @andreypopp, good thing you used that reply button so I got the notification to my email.

Nice, I think you got the basis right. The issue with React-based NodeViews isn’t that it’s undoable, it is just that the interfacing & gluing between the NodeViews, Schema and Plugins (with the event dispatching and portals) is quite difficult to make.

So immediately if you want to create a plugin system on top of that implementation, you’ll have to start thinking how to join all the parts (normal plugins + schema + nodeviews (as the React components) + possible toolbar buttons & actions) together. What I got stuck before I got too busy doing other things was the implementing of the event-flow from the actions/key-press plugins to the plugin state, which would then notify the React components through an event-dispatcher. There’s an example in the Atlassian repo if you want to a crack at it.

1 Like

Yeah, I can see that the difficulties are elsewhere.

Btw I’m not sure I want to use portals just yet, the separate roots should probably work fine. Why do you think portals are necessary?

FWIW I don’t think that PM plugin system should be “pure” React, the current abstractions in PM are quite well designed so I intend to keep using the PM API as is. OTOH, nothing prevents from composing a custom editor plugin right inside of a React component based on props.

What I’ve stumbled upon is the problem with nested DOM structures (which will happen quite a lot with React based node views) in NodeView (contentDOM is nested deeply inside dom) and how it’s all parsed by PM on DOM modfications (logged an issue here)

And then I see @johnkueh you’ve done quite a decent starter for the React + PM combo. Very impressive. You got the whole plumbing into a simple React hook, that’s suberb!

If you want to hear my opinion/feedback, I would maybe separate some stuff from the ReactNodeView as it seems a bit cluttered. Also I’m curious about how you use the ReactDOM.createPortal, will the React components be unmounted correctly when they are destroyed? There’s no unmountComponentAtNode() call so I’m not sure does it cause a memory leak.

How in Atlassian’s editor they made it was using a separate portalProvider that was passed down to the ReactNodeView. Also just as I mentioned to andrey, if you want to have the React NodeViews be notified of the plugin state changes you need some sort of event-dispatcher. I was in the middle of devising a react-redux type of addStateToProps HOC, but I was too busy with other stuff. If you have time and interest, you could try that or even create it as a hook. That would be amazing =)! Eg usePluginState(PLUGIN_KEY).

Also does using React’s context add some benefit compared to just passing the NodeView’s attributes down as props to the component? Atlassian’s editor, at some point at least, used extensively context which made the resulting React component tree become like 100 components deep, it seemed like a design mistake.

That’s a good question about the portals. I guess with portals the React nodes will remain in one same React tree, so they can share/use the same React context. And maybe some other advantages, I’m not sure really. I myself noted that Atlassian’s editor used it and it seemed like a use-case that was fit for it.

Yes I agree totally the plugin system shouldn’t React-based per say. It’s just that if you want to have modular plugins that include all those aspects I mentioned, you have to combine them yourself into one custom plugin, as the way you pass nodeviews, plugins etc to PM is through several different APIs.

Eg schema and plugins are provided through editorState yet nodeviews through editorView. Then because editorState is instantiated before the editorView, your plugins won’t have access to it until it is created. So if you are using something like Mobx stores with both the state and the action dispatchers, you need a separate init(editorView) to add the view afterwards. Which is I guess fine but it’s bit awkward. And well I switched from Mobx to Redux since PM kinda forces you to use Redux type application logic.

And oh you also got that problem :sweat_smile:. I think I actually stumbled upon the same thing. In my case I was using lot of inline nodes, so I had to modify the delete functionality to account for the extra offsets with positions. It’s been a long time since I solved it, like over a half of year, so I don’t really remember to exact details. Use the ProseMirror devtools to at least see how the elements behave, and maybe @marijn will tell you how it’s done.

This thread, and also this one https://discuss.prosemirror.net/t/lightweight-react-integration-example/2680 helped me get a very basic editor with persistence to a CQRS-based backend up and running–but with some horrible re-render jank that causes the editablecontent to lose focus.

For those of you using strategies put forth in this thread, can you share your persistence strategy? (both directions)

Hello everyone,

Here is my take on using ProseMirror with React: https://github.com/dminkovsky/use-prosemirror.

It is a minimal, un-opinionated integration that aims to embrace the similarity between the React and ProseMirror render models. It:

  • Separates state and presentation, so you can keep your state as high up as necessary.
  • Allows using EditorView props as React props.
  • Is written in TypeScript.

Feedback and contributions would be most welcome!

7 Likes