diff --git a/.changeset/thick-months-train.md b/.changeset/thick-months-train.md new file mode 100644 index 0000000000..c6b3c4b61e --- /dev/null +++ b/.changeset/thick-months-train.md @@ -0,0 +1,5 @@ +--- +'@tiptap/react': patch +--- + +Fix race conditions in ReactRenderer causing destroyed renderers to be re-added in Strict Mode diff --git a/packages/react/src/ReactRenderer.tsx b/packages/react/src/ReactRenderer.tsx index cd6747160b..7471a22847 100644 --- a/packages/react/src/ReactRenderer.tsx +++ b/packages/react/src/ReactRenderer.tsx @@ -159,6 +159,11 @@ export class ReactRenderer = object> ref: R | null = null + /** + * Flag to track if the renderer has been destroyed, preventing queued or asynchronous renders from executing after teardown. + */ + destroyed = false + /** * Immediately creates element and renders the provided React component. */ @@ -186,6 +191,9 @@ export class ReactRenderer = object> }) } else { queueMicrotask(() => { + if (this.destroyed) { + return + } this.render() }) } @@ -195,6 +203,10 @@ export class ReactRenderer = object> * Render the React component. */ render(): void { + if (this.destroyed) { + return + } + const Component = this.component const props = this.props const editor = this.editor as EditorWithContentComponent @@ -227,6 +239,10 @@ export class ReactRenderer = object> * Re-renders the React component with new props. */ updateProps(props: Record = {}): void { + if (this.destroyed) { + return + } + this.props = { ...this.props, ...props, @@ -239,6 +255,7 @@ export class ReactRenderer = object> * Destroy the React component. */ destroy(): void { + this.destroyed = true const editor = this.editor as EditorWithContentComponent editor?.contentComponent?.removeRenderer(this.id)