Skip to content

Commit f1f5f19

Browse files
authored
Fully split CustomNodeView into a separate, memoized component (#12)
Plain custom node views had been broken since we implemented SSR. When they were pulled into their own component, it messed up some of the layout effect ordering and triggering, such that we no longer attached them to the DOM. To resolve this, the NodeView component now either renders ReactNodeView or CustomNodeView, each of which manages its own full lifecycle, decorations, etc.
1 parent ce158a4 commit f1f5f19

File tree

4 files changed

+296
-209
lines changed

4 files changed

+296
-209
lines changed

.yarn/versions/1cfdbe48.yml

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
releases:
2+
"@handlewithcare/react-prosemirror": patch

src/components/CustomNodeView.tsx

+131-17
Original file line numberDiff line numberDiff line change
@@ -5,42 +5,139 @@ import {
55
NodeViewConstructor,
66
NodeView as NodeViewT,
77
} from "prosemirror-view";
8-
import React, { MutableRefObject, createElement, useContext } from "react";
8+
import React, {
9+
MutableRefObject,
10+
cloneElement,
11+
createElement,
12+
memo,
13+
useContext,
14+
useLayoutEffect,
15+
useMemo,
16+
useRef,
17+
} from "react";
918
import { createPortal } from "react-dom";
1019

20+
import { ChildDescriptorsContext } from "../contexts/ChildDescriptorsContext.js";
1121
import { EditorContext } from "../contexts/EditorContext.js";
1222
import { useClientOnly } from "../hooks/useClientOnly.js";
23+
import { useNodeViewDescriptor } from "../hooks/useNodeViewDescriptor.js";
1324

14-
import { ChildNodeViews } from "./ChildNodeViews.js";
25+
import { ChildNodeViews, wrapInDeco } from "./ChildNodeViews.js";
1526

1627
interface Props {
17-
customNodeViewRootRef: MutableRefObject<HTMLDivElement | null>;
18-
customNodeViewRef: MutableRefObject<NodeViewT | null>;
19-
contentDomRef: MutableRefObject<HTMLElement | null>;
2028
customNodeView: NodeViewConstructor;
21-
initialNode: MutableRefObject<Node>;
2229
node: Node;
2330
getPos: MutableRefObject<() => number>;
24-
initialOuterDeco: MutableRefObject<readonly Decoration[]>;
25-
initialInnerDeco: MutableRefObject<DecorationSource>;
2631
innerDeco: DecorationSource;
32+
outerDeco: readonly Decoration[];
2733
}
2834

29-
export function CustomNodeView({
30-
contentDomRef,
31-
customNodeViewRef,
32-
customNodeViewRootRef,
35+
export const CustomNodeView = memo(function CustomNodeView({
3336
customNodeView,
34-
initialNode,
3537
node,
3638
getPos,
37-
initialOuterDeco,
38-
initialInnerDeco,
3939
innerDeco,
40+
outerDeco,
4041
}: Props) {
4142
const { view } = useContext(EditorContext);
43+
const domRef = useRef<HTMLElement | null>(null);
44+
const nodeDomRef = useRef<HTMLElement | null>(null);
45+
const contentDomRef = useRef<HTMLElement | null>(null);
46+
const getPosFunc = useRef(() => getPos.current()).current;
47+
48+
// this is ill-conceived; should revisit
49+
const initialNode = useRef(node);
50+
const initialOuterDeco = useRef(outerDeco);
51+
const initialInnerDeco = useRef(innerDeco);
52+
53+
const customNodeViewRootRef = useRef<HTMLDivElement | null>(null);
54+
const customNodeViewRef = useRef<NodeViewT | null>(null);
4255

4356
const shouldRender = useClientOnly();
57+
58+
useLayoutEffect(() => {
59+
if (
60+
!customNodeViewRef.current ||
61+
!customNodeViewRootRef.current ||
62+
!shouldRender
63+
)
64+
return;
65+
66+
const { dom } = customNodeViewRef.current;
67+
nodeDomRef.current = customNodeViewRootRef.current;
68+
customNodeViewRootRef.current.appendChild(dom);
69+
return () => {
70+
customNodeViewRef.current?.destroy?.();
71+
};
72+
}, [customNodeViewRef, customNodeViewRootRef, nodeDomRef, shouldRender]);
73+
74+
useLayoutEffect(() => {
75+
if (!customNodeView || !customNodeViewRef.current || !shouldRender) return;
76+
77+
const { destroy, update } = customNodeViewRef.current;
78+
79+
const updated =
80+
update?.call(customNodeViewRef.current, node, outerDeco, innerDeco) ??
81+
true;
82+
if (updated) return;
83+
84+
destroy?.call(customNodeViewRef.current);
85+
86+
if (!customNodeViewRootRef.current) return;
87+
88+
initialNode.current = node;
89+
initialOuterDeco.current = outerDeco;
90+
initialInnerDeco.current = innerDeco;
91+
92+
customNodeViewRef.current = customNodeView(
93+
initialNode.current,
94+
// customNodeView will only be set if view is set, and we can only reach
95+
// this line if customNodeView is set
96+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
97+
view!,
98+
getPosFunc,
99+
initialOuterDeco.current,
100+
initialInnerDeco.current
101+
);
102+
const { dom } = customNodeViewRef.current;
103+
nodeDomRef.current = customNodeViewRootRef.current;
104+
customNodeViewRootRef.current.appendChild(dom);
105+
}, [
106+
customNodeView,
107+
view,
108+
innerDeco,
109+
node,
110+
outerDeco,
111+
getPos,
112+
customNodeViewRef,
113+
customNodeViewRootRef,
114+
initialNode,
115+
initialOuterDeco,
116+
initialInnerDeco,
117+
nodeDomRef,
118+
shouldRender,
119+
getPosFunc,
120+
]);
121+
122+
const { childDescriptors, nodeViewDescRef } = useNodeViewDescriptor(
123+
node,
124+
() => getPos.current(),
125+
domRef,
126+
nodeDomRef,
127+
innerDeco,
128+
outerDeco,
129+
undefined,
130+
contentDomRef
131+
);
132+
133+
const childContextValue = useMemo(
134+
() => ({
135+
parentRef: nodeViewDescRef,
136+
siblingsRef: childDescriptors,
137+
}),
138+
[childDescriptors, nodeViewDescRef]
139+
);
140+
44141
if (!shouldRender) return null;
45142

46143
if (!customNodeViewRef.current) {
@@ -57,7 +154,7 @@ export function CustomNodeView({
57154
}
58155
const { contentDOM } = customNodeViewRef.current;
59156
contentDomRef.current = contentDOM ?? null;
60-
return createElement(
157+
const element = createElement(
61158
node.isInline ? "span" : "div",
62159
{
63160
ref: customNodeViewRootRef,
@@ -74,4 +171,21 @@ export function CustomNodeView({
74171
contentDOM
75172
)
76173
);
77-
}
174+
175+
const decoratedElement = cloneElement(
176+
outerDeco.reduce(wrapInDeco, element),
177+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
178+
outerDeco.some((d) => (d as any).type.attrs.nodeName)
179+
? { ref: domRef }
180+
: // If all of the node decorations were attr-only, then
181+
// we've already passed the domRef to the NodeView component
182+
// as a prop
183+
undefined
184+
);
185+
186+
return (
187+
<ChildDescriptorsContext.Provider value={childContextValue}>
188+
{decoratedElement}
189+
</ChildDescriptorsContext.Provider>
190+
);
191+
});

0 commit comments

Comments
 (0)