Skip to content

Commit f5bcf36

Browse files
committed
fix(react): destroy editor instances after two ticks
React's StrictMode makes it so that refs (which we use as the main trigger for mounting/unmounting the editor), are triggered twice. This makes it difficult to know when the editor is _actually_ meant to be unmounted. If an editor is unmounted during a render of React NodeViews, this can cause errors to occur (since elements which were in the dom at the start of the render are no longer in the dom by the time that the render occurs). This change takes a similar approach to the editor integration in Tiptap, where it delays `unmount`ing the editor until 2 ticks have passed. This is to allow React the opportunity to re-call `mount` with the same element, or if the ticks pass, to actually unmount the editor instance. In most cases, delaying the removal of the editor instance should be fine, because it is not expected to be available after it has been destroyed.
1 parent a17a960 commit f5bcf36

File tree

1 file changed

+28
-3
lines changed

1 file changed

+28
-3
lines changed

packages/core/src/editor/BlockNoteEditor.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1035,15 +1035,40 @@ export class BlockNoteEditor<
10351035
* @warning Not needed to call manually when using React, use BlockNoteView to take care of mounting
10361036
*/
10371037
public mount = (element: HTMLElement) => {
1038-
// TODO: Fix typing for this in a TipTap PR
1039-
this._tiptapEditor.mount({ mount: element } as any);
1038+
if (
1039+
// If the editor is scheduled for destruction, and
1040+
this.scheduledDestructionTimeout &&
1041+
// If the editor is being remounted to the same element as the one which is scheduled for destruction,
1042+
// then just cancel the destruction timeout
1043+
this.prosemirrorView.dom === element
1044+
) {
1045+
clearTimeout(this.scheduledDestructionTimeout);
1046+
this.scheduledDestructionTimeout = undefined;
1047+
return;
1048+
}
1049+
1050+
this._tiptapEditor.mount({ mount: element });
10401051
};
10411052

1053+
/**
1054+
* Timeout to schedule the {@link unmount}ing of the editor.
1055+
*/
1056+
private scheduledDestructionTimeout:
1057+
| ReturnType<typeof setTimeout>
1058+
| undefined = undefined;
1059+
10421060
/**
10431061
* Unmount the editor from the DOM element it is bound to
10441062
*/
10451063
public unmount = () => {
1046-
this._tiptapEditor.unmount();
1064+
// Due to how React's StrictMode works, it will `unmount` & `mount` the component twice in development mode.
1065+
// This can result in the editor being unmounted mid-rendering the content of node views.
1066+
// To avoid this, we only ever schedule the `unmount`ing of the editor when we've seen whether React "meant" to actually unmount the editor (i.e. not calling mount one tick later).
1067+
// So, we wait two ticks to see if the component is still meant to be unmounted, and if not, we actually unmount the editor.
1068+
this.scheduledDestructionTimeout = setTimeout(() => {
1069+
this._tiptapEditor.unmount();
1070+
this.scheduledDestructionTimeout = undefined;
1071+
}, 1);
10471072
};
10481073

10491074
/**

0 commit comments

Comments
 (0)