Skip to content

Commit c493d85

Browse files
authored
fix(react): destroy editor instances after two ticks (#2121)
1 parent a17a960 commit c493d85

File tree

4 files changed

+228
-193
lines changed

4 files changed

+228
-193
lines changed

packages/core/src/editor/BlockNoteEditor.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ it("block prop types", () => {
104104
}
105105
});
106106

107-
it("onMount and onUnmount", () => {
107+
it("onMount and onUnmount", async () => {
108108
const editor = BlockNoteEditor.create();
109109
let mounted = false;
110110
let unmounted = false;
@@ -118,6 +118,10 @@ it("onMount and onUnmount", () => {
118118
expect(mounted).toBe(true);
119119
expect(unmounted).toBe(false);
120120
editor.unmount();
121+
// expect the unmount event to not have been triggered yet, since it waits 2 ticks
122+
expect(unmounted).toBe(false);
123+
// wait 3 ticks to ensure the unmount event is triggered
124+
await new Promise((resolve) => setTimeout(resolve, 3));
121125
expect(mounted).toBe(true);
122126
expect(unmounted).toBe(true);
123127
});

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
/**

packages/server-util/src/context/ServerBlockNoteEditor.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@ export class ServerBlockNoteEditor<
332332
return await fn();
333333
} finally {
334334
tmpRoot.unmount();
335+
await new Promise((resolve) => setTimeout(resolve, 3));
335336
}
336337
});
337338
}

0 commit comments

Comments
 (0)