Skip to content

Conversation

@YousefED
Copy link
Collaborator

@YousefED YousefED commented Jan 6, 2026

Fix LinkToolbar Event Listener Leak on Remount

Summary

This PR addresses an issue where event listeners were not being correctly removed in LinkToolbarController during editor remounts, and adds a regression test to verify the fix.

closes #2327

Rationale

When the editor component is unmounted or rapidly re-mounted, the cleanup function in useEffect could previously fail to remove the mouseover event listener if the underlying domElement reference had changed or become undefined. This changes ensures removeEventListener is always called on the correct element instance that the listener was originally attached to.

Changes

  • Fix LinkToolbarController: captured the element in the useEffect scope to ensure removeEventListener is called on the correct element during cleanup.
  • Safety Fixes: applied the same pattern to similar parts of the codebase to prevent consistent issues.
  • New Test: added unit test to reproduce
  • Discussion Points: Added comments marked in the code with follow-up questions to discuss with @nperez0111 @matthewlipski

Impact

Prevents runtime errors and potential memory leaks when the editor is mounted and unmounted frequently

Testing

  • tested manually
  • unit test added

Screenshots/Video

N/A

Checklist

  • Code follows the project's coding standards.
  • Unit tests covering the new feature have been added.
  • All existing tests pass.
  • The documentation has been updated to reflect the new feature

Additional Notes

I've left several comments in the code raising questions about the current architecture for handling these event listeners, which we should discuss in a future refactor.

@vercel
Copy link

vercel bot commented Jan 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
blocknote Ready Ready Preview Jan 7, 2026 0:33am
blocknote-website Ready Ready Preview Jan 7, 2026 0:33am

@pkg-pr-new
Copy link

pkg-pr-new bot commented Jan 6, 2026

Open in StackBlitz

@blocknote/ariakit

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/ariakit@2335

@blocknote/code-block

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/code-block@2335

@blocknote/core

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/core@2335

@blocknote/mantine

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/mantine@2335

@blocknote/react

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/react@2335

@blocknote/server-util

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/server-util@2335

@blocknote/shadcn

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/shadcn@2335

@blocknote/xl-ai

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-ai@2335

@blocknote/xl-docx-exporter

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-docx-exporter@2335

@blocknote/xl-email-exporter

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-email-exporter@2335

@blocknote/xl-multi-column

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-multi-column@2335

@blocknote/xl-odt-exporter

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-odt-exporter@2335

@blocknote/xl-pdf-exporter

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-pdf-exporter@2335

commit: c4c43da

editor.domElement?.addEventListener("mouseover", mouseCursorCallback);
const domElement = editor.domElement;

// Q 1: why can domElement be available when <LinkToolbarController/> is rendered?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would domElement not be available? It would seem to me that is sort of the default state of things?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops my comment was wrong. Question should be why it can be unavailable (i.e. undefined).

const domElement = editor.domElement;

// Q 1: why can domElement be available when <LinkToolbarController/> is rendered?
// Q 2: this useEffect will not necessarily run when editor.domElement changes
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to enable to react eslint plugin, it would have caught this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's just a matter of eslint; because the component will also not automatically rerender when domElement changes; accessing the domElement property is non-reactive, so it's "dangerous" to depend on this state from the React layer?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this this observed, or a hypothetical?

because the component will also not automatically rerender when domElement changes;

My understanding is that this reference should be stable across renders.

[link?.element],
);

// Q3: similar to Q2; are we sure the component rerenders when editor.isEditable changes?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be derived from a useEditorState, rather than queried at render time

Comment on lines 70 to 73
// Q4: posAtDOM can fail if the editor view is not available
// (e.g. if the editor is not mounted)
// a) Unfortunately, TS doesn't give an error about this. Can we make this type safe?
// b) Double check other references of editor.prosemirrorView
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

posAtDom fails, or does accessing editor.prosemirrorView?

TS could never know if posAtDom fails at runtime
TS could say the editor.prosemirrorView is undefined

anytime editor.prosemirrorView or editor.prosemirrorState is used, it should be considered a failing of our API

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

accessing posAtDom fails fails (because there's no underlying view in the tiptap layer).

anytime editor.prosemirrorView or editor.prosemirrorState is used, it should be considered a failing of our API

Agree, this seems like the root cause. Will start eng. roadmap and add this

@nperez0111 nperez0111 merged commit eb48e3e into main Jan 7, 2026
8 checks passed
@nperez0111 nperez0111 deleted the fix/remount-issue-link-toolbar branch January 7, 2026 12:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

posAtDOM error when editor view remounts (locale/editable change)

3 participants