From 951347896367ad3f2dae28b5cb534402b993dec8 Mon Sep 17 00:00:00 2001 From: Jeff Sisson Date: Tue, 17 Mar 2026 11:03:37 -0400 Subject: [PATCH 1/2] fixes bug with useEditorEffect being called with an already-unmounted EditorView --- .../useEditorViewLayoutEffect.test.tsx | 106 +++++++++++++++++- src/hooks/useEditorEffect.ts | 2 +- 2 files changed, 105 insertions(+), 3 deletions(-) diff --git a/src/hooks/__tests__/useEditorViewLayoutEffect.test.tsx b/src/hooks/__tests__/useEditorViewLayoutEffect.test.tsx index 20ce83e3..10d6ca20 100644 --- a/src/hooks/__tests__/useEditorViewLayoutEffect.test.tsx +++ b/src/hooks/__tests__/useEditorViewLayoutEffect.test.tsx @@ -1,12 +1,17 @@ /* eslint-disable @typescript-eslint/no-empty-function */ import { render } from "@testing-library/react"; -import type { EditorState } from "prosemirror-state"; -import type { EditorView } from "prosemirror-view"; +import { Schema } from "prosemirror-model"; +import { EditorState } from "prosemirror-state"; +import { EditorView } from "prosemirror-view"; import React from "react"; import { LayoutGroup } from "../../components/LayoutGroup.js"; import { EditorContext } from "../../contexts/EditorContext.js"; +import { + setupProseMirrorView, + teardownProseMirrorView, +} from "../../testing/setupProseMirrorView.js"; import { useEditorEffect } from "../useEditorEffect.js"; function TestComponent({ @@ -126,4 +131,101 @@ describe("useEditorViewLayoutEffect", () => { expect(effect).toHaveBeenCalledTimes(2); }); + + describe("with a real EditorView", () => { + const schema = new Schema({ + nodes: { + text: {}, + doc: { content: "text*" }, + }, + }); + + beforeAll(() => { + setupProseMirrorView(); + }); + + afterAll(() => { + teardownProseMirrorView(); + }); + + it("should not run the effect if the EditorView has been destroyed", () => { + const editorState = EditorState.create({ schema }); + const mount = document.createElement("div"); + document.body.appendChild(mount); + const editorView = new EditorView({ mount }, { state: editorState }); + + editorView.destroy(); + expect(editorView.isDestroyed).toBe(true); + + const effect = jest.fn(); + + render( + + {}, + unregisterEventListener: () => {}, + }} + > + + + + ); + + expect(effect).not.toHaveBeenCalled(); + + document.body.removeChild(mount); + }); + + it("should not re-run the effect after the EditorView is destroyed between renders", () => { + const editorState = EditorState.create({ schema }); + const mount = document.createElement("div"); + document.body.appendChild(mount); + const editorView = new EditorView({ mount }, { state: editorState }); + + const effect = jest.fn(); + + const { rerender } = render( + + {}, + unregisterEventListener: () => {}, + }} + > + + + + ); + + expect(effect).toHaveBeenCalledTimes(1); + expect(effect).toHaveBeenCalledWith(editorView); + + editorView.destroy(); + expect(editorView.isDestroyed).toBe(true); + + rerender( + + {}, + unregisterEventListener: () => {}, + }} + > + + + + ); + + expect(effect).toHaveBeenCalledTimes(1); + + document.body.removeChild(mount); + }); + }); }); diff --git a/src/hooks/useEditorEffect.ts b/src/hooks/useEditorEffect.ts index 007d61df..4c2e177e 100644 --- a/src/hooks/useEditorEffect.ts +++ b/src/hooks/useEditorEffect.ts @@ -34,7 +34,7 @@ export function useEditorEffect( // be defined inline and run on every re-render. useLayoutGroupEffect( () => { - if (editorView) { + if (editorView && !editorView.isDestroyed) { return effect(editorView); } }, From 4179a389cf835a0b4934ffb8fe3d0c7846a2c049 Mon Sep 17 00:00:00 2001 From: Jeff Sisson Date: Tue, 17 Mar 2026 11:38:20 -0400 Subject: [PATCH 2/2] schedule a patch release anyways, even though we haven't published the major release yet --- .yarn/versions/a3c9f4be.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .yarn/versions/a3c9f4be.yml diff --git a/.yarn/versions/a3c9f4be.yml b/.yarn/versions/a3c9f4be.yml new file mode 100644 index 00000000..90948e78 --- /dev/null +++ b/.yarn/versions/a3c9f4be.yml @@ -0,0 +1,2 @@ +releases: + "@nytimes/react-prosemirror": patch