I’m trying to render custom React component inside a node. When using React 18’s new createRoot
and unmount
methods for mounting and unmounting the components, I encounter an infinite loop of re-renders. However, switching back to the deprecated ReactDOM.render
and ReactDOM.unmountComponentAtNode
methods works as expected without causing re-render issues. I tried to switch to createPortal, but it also causes the same issues.
What might be causing the infinite loop of re-renders in this context?
Working Code (With Deprecated Methods):
import { Node } from 'prosemirror-model';
import { Decoration, EditorView, NodeView } from 'prosemirror-view';
import React, { useContext } from 'react';
import { ThemeProvider } from 'styled-components';
import ReactDOM from 'react-dom';
import Theme from '../../../../theme';
interface IReactNodeViewContext {
node: Node;
view: EditorView;
getPos: () => number | undefined;
decorations: readonly Decoration[];
}
const ReactNodeViewContext = React.createContext<
Partial<IReactNodeViewContext>
>({
node: undefined,
view: undefined,
getPos: undefined,
decorations: undefined,
});
class ReactNodeView implements NodeView {
contentDOM?: HTMLElement | null;
component: React.FC<any>;
node: Node;
view: EditorView;
getPos: () => number | undefined;
decorations: readonly Decoration[];
dom: HTMLElement;
constructor(
node: Node,
view: EditorView,
getPos: () => number | undefined,
decorations: readonly Decoration[],
component: React.FC<any>
) {
this.node = node;
this.view = view;
this.getPos = getPos;
this.decorations = decorations;
this.component = component;
this.dom = document.createElement('div');
this.contentDOM = document.createElement('div');
}
init() {
this.dom.classList.add('claimAdvantage');
this.render();
return {
nodeView: this,
};
}
render = (): void => {
ReactDOM.render(
<ThemeProvider theme={Theme}>
<ReactNodeViewContext.Provider
value={{
node: this.node,
view: this.view,
getPos: this.getPos,
decorations: this.decorations,
}}
>
<this.component contentDOMRef={this.handleRef} />
</ReactNodeViewContext.Provider>
</ThemeProvider>,
this.dom
);
};
handleRef = (node: HTMLElement | null): void => {
if (node && this.contentDOM && !node.contains(this.contentDOM)) {
node.appendChild(this.contentDOM);
}
};
update(node: Node) {
if (this.node.type !== node.type) {
return false;
}
this.node = node;
this.render();
return true;
}
destroy() {
ReactDOM.unmountComponentAtNode(this.dom);
}
}
interface TCreateReactNodeView extends IReactNodeViewContext {
component: React.FC<any>;
}
export const createReactNodeView = ({
node,
view,
getPos,
decorations,
component,
}: TCreateReactNodeView) => {
const reactNodeView = new ReactNodeView(
node,
view,
getPos,
decorations,
component
);
const { nodeView } = reactNodeView.init();
return nodeView;
};
export const useReactNodeView = () => useContext(ReactNodeViewContext);
export default ReactNodeView;
Non-Working Code (With React 18 Methods):
import React, { useContext } from 'react';
import ReactDOM from 'react-dom/client';
import { ThemeProvider } from 'styled-components';
import { Node } from 'prosemirror-model';
import { Decoration, EditorView, NodeView } from 'prosemirror-view';
import Theme from '../../../../theme';
interface IReactNodeViewContext {
node: Node;
view: EditorView;
getPos: () => number | undefined;
decorations: readonly Decoration[];
}
const ReactNodeViewContext = React.createContext<
Partial<IReactNodeViewContext>
>({
node: undefined,
view: undefined,
getPos: undefined,
decorations: undefined,
});
class ReactNodeView implements NodeView {
contentDOM?: HTMLElement | null;
component: React.FC<any>;
node: Node;
view: EditorView;
getPos: () => number | undefined;
decorations: readonly Decoration[];
dom: HTMLElement;
root: ReactDOM.Root;
constructor(
node: Node,
view: EditorView,
getPos: () => number | undefined,
decorations: readonly Decoration[],
component: React.FC<any>
) {
this.node = node;
this.view = view;
this.getPos = getPos;
this.decorations = decorations;
this.component = component;
this.dom = document.createElement('div');
this.contentDOM = document.createElement('div');
this.root = ReactDOM.createRoot(this.dom);
}
init() {
this.dom.classList.add('claimAdvantage');
this.render();
return {
nodeView: this,
};
}
render = (): void => {
this.root.render(
<ThemeProvider theme={Theme}>
<ReactNodeViewContext.Provider
value={{
node: this.node,
view: this.view,
getPos: this.getPos,
decorations: this.decorations,
}}
>
<this.component contentDOMRef={this.handleRef} />
</ReactNodeViewContext.Provider>
</ThemeProvider>
);
};
handleRef = (node: HTMLElement | null): void => {
if (node && this.contentDOM && !node.contains(this.contentDOM)) {
node.appendChild(this.contentDOM);
}
};
update(node: Node) {
if (this.node.type !== node.type) {
return false;
}
this.node = node;
this.render();
return true;
}
destroy() {
this.root.unmount();
}
}
interface TCreateReactNodeView extends IReactNodeViewContext {
component: React.FC<any>;
}
export const createReactNodeView = ({
node,
view,
getPos,
decorations,
component,
}: TCreateReactNodeView) => {
const reactNodeView = new ReactNodeView(
node,
view,
getPos,
decorations,
component
);
const { nodeView } = reactNodeView.init();
return nodeView;
};
export const useReactNodeView = () => useContext(ReactNodeViewContext);
export default ReactNodeView;