Rendering React components (or similar) in Decoration.toDOM()?

Hi, I’m working on an editor that replaces certain AST nodes with interactive components when possible. Similar to the true/false checkbox example, but extended to things like augmenting [r, g, b] or [h, s, l] with a RGB or HSL color picker.

In my app, an React component renders the Codemirror editor and maintains a ref to its view. Is it possible to somehow return a React component in the toDOM method of a Decoration, or replace toDOM with something else capable of rendering a React component in the same tree as the Editor component?

I would like to render Decorations using React because it most closely matches how I build UI throughout the rest of my app, and would allow me to use the same components (e.g. color picker) in the editor and in an exported “view mode”. Otherwise, I would need to rewrite the rendering code for things like the color picker to be vanilla, which I would prefer not to do.

Thank you!

I generally recommend against this, since it introduces a bunch of extra indirection an inefficiency that, for all but the most complicated widgets, isn’t going to pay off, but I believe react has a feature called portals that may help with this—have the toDOM method return a parent node, and then asynchronously wire it up to a react portal.

3 Likes

Using the element returned from toDOM as a portal container did not work for me. Anything that I attach to the container element from my React code gets deleted.

you can do something like this to render react component inside a dom element then return the dom element

const dom = document.createElement('div')
  const root = ReactDOM.createRoot(dom)
  root.render(
    <Reactcomponent />
  )
  return { dom }

Using React Portal works pretty well for this.For the widget, just create an empty DOM element. Then pass a reference of this back to your React code (I instantiate the plugin from React and have the plugin return the widget instance).

class PortalWidget extends WidgetType {
  container: HTMLElement;
  constructor() {
    super();
    const container = document.createElement("span");
    container.className = "relative";
    this.container = container;
  }
  toDOM() {
    return this.container;
  }
  ignoreEvent() {
    return false;
  }
}

function createMyPlugin() {
  const widget = new PortalWidget();

  return {
    widget,
    // anything else you want your plugin to do, for example a decoration that adds the widget to the editor
  };
}

And then using Radix UI’s Portal:

import * as Portal from "@radix-ui/react-portal";

const myPlugin = createMyPlugin();
...

    <Portal.Root container={myPlugin.widget.container}>
      hello from React
    </Portal.Root>