Skip to content

fix: Editor re-render on mount#2619

Open
matthewlipski wants to merge 4 commits intomainfrom
rerender-on-mount
Open

fix: Editor re-render on mount#2619
matthewlipski wants to merge 4 commits intomainfrom
rerender-on-mount

Conversation

@matthewlipski
Copy link
Copy Markdown
Collaborator

@matthewlipski matthewlipski commented Apr 2, 2026

Summary

This PR makes the editor container component re-render when the editor view is mounted. This fixes certain cases where descendants rely on editor.domElement to be defined. However, when it becomes defined, there is nothing which explicitly triggers a re-render, meaning things like event listeners may not get attached until some other unrelated effect triggers a re-render.

Closes #2546

Rationale

See above.

Changes

  • Made BlockNoteEditor onMount and onUnmount return callbacks to destroy the listeners.
  • Made BlockNoteViewComponent force a re-render when the TipTap editor's create event fires.

Impact

N/A

Testing

I wasn't able to reproduce the issue in #2546 locally, and have not seen other cases of this elsewhere. Therefore, no tests have been 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

Summary by CodeRabbit

  • New Features
    • Added useEditorDOMElement() hook for accessing the editor's DOM element in React components.
    • onMount() and onUnmount() methods now return unsubscribe/removal functions for improved event listener management.

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 2, 2026

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

Project Deployment Actions Updated (UTC)
blocknote Ready Ready Preview Apr 16, 2026 2:14pm
blocknote-website Error Error Apr 16, 2026 2:14pm

Request Review

@matthewlipski matthewlipski requested a review from nperez0111 April 2, 2026 12:23
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 2, 2026

📝 Walkthrough

Walkthrough

This PR introduces a new React hook useEditorDOMElement() that reactively tracks editor DOM element initialization with TipTap event listening, and refactors multiple components to use it instead of directly accessing editor.domElement. Additionally, BlockNoteEditor methods now return propagated values from the event manager. These changes ensure proper DOM element timing for hover-dependent UI features.

Changes

Cohort / File(s) Summary
Core Editor Return Values
packages/core/src/editor/BlockNoteEditor.ts
onMount and onUnmount methods now return propagated values from _eventManager instead of void.
New Editor DOM Element Hook
packages/react/src/hooks/useEditorDomElement.ts, packages/react/src/index.ts
Added new useEditorDOMElement hook that reactively obtains editor DOM element with TipTap "create" event listener initialization; re-exported in public API.
Link and Formatting Toolbar Components
packages/react/src/components/LinkToolbar/LinkToolbarController.tsx, packages/react/src/components/FormattingToolbar/DefaultButtons/CreateLinkButton.tsx
Refactored to use useEditorDOMElement() hook for obtaining editor DOM reference instead of direct editor.domElement access; updated effect dependencies.
Floating UI Reference Components
packages/react/src/components/Popovers/PositionPopover.tsx, packages/react/src/components/SuggestionMenu/.../GridSuggestionMenuController.tsx, packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx
Updated to use useEditorDOMElement() hook for FloatingUI reference element and context element configuration.
Keyboard Navigation Hooks
packages/react/src/components/SuggestionMenu/.../useGridSuggestionMenuKeyboardNavigation.ts, packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts
Refactored to obtain editor DOM element via useEditorDOMElement() hook; editor parameter renamed to _editor to indicate unused status.

Sequence Diagram

sequenceDiagram
    participant Component as React Component
    participant Hook as useEditorDOMElement()
    participant Editor as BlockNoteEditor
    participant TipTap as TipTap Editor
    participant State as useEditorState()
    
    Component->>Hook: Call useEditorDOMElement()
    Hook->>Editor: useBlockNoteEditor() - get editor
    alt editor.headless is true
        Hook->>TipTap: Register "create" event listener on _tiptapEditor
        TipTap-->>Hook: "create" event fires during initialization
        Hook->>Hook: Set initialized = true
    end
    Hook->>State: useEditorState() - track domElement
    State->>Editor: Select ctx.editor?.domElement
    State-->>Hook: Return reactive editorDOMElement value
    Hook-->>Component: editorDOMElement
    Component->>Component: Update event listeners/references with editorDOMElement
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • nperez0111

Poem

🐰 A new hook hops into view,
Waiting for TipTap to debut,
Linking menus now appear on sight,
The DOM element timed just right! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: Editor re-render on mount' directly and clearly summarizes the main change—forcing a component re-render when the editor mounts to resolve DOM element availability issues.
Description check ✅ Passed The PR description covers all major template sections: Summary, Rationale, Changes, Impact, Testing, and Checklist. While some sections are brief, all required information is present and the description adequately explains the problem and solution.
Linked Issues check ✅ Passed The PR successfully addresses issue #2546 by introducing useEditorDOMElement() hook to provide reactive DOM element updates and ensuring the editor component re-renders when the TipTap 'create' event fires, enabling dependent components to attach event listeners promptly.
Out of Scope Changes check ✅ Passed All changes are in scope: modified BlockNoteEditor return values, created new useEditorDOMElement hook, and refactored multiple React components to use the new hook for consistent DOM element resolution—all supporting the re-render-on-mount objective.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch rerender-on-mount

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd really prefer not to do this if at all possible. This causes jank for everyone else. I'd prefer something at the read-side (e.g. making the reading of the domElement reactive, or making the component that needs this element on render to remount). But remounting the whole editor is way too much

Copy link
Copy Markdown
Contributor

@nperez0111 nperez0111 left a comment

Choose a reason for hiding this comment

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

I'd prefer a different solution here, this is too much. Especially for an issue we can't reliably reproduce

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 16, 2026

Open in StackBlitz

@blocknote/ariakit

npm i https://pkg.pr.new/@blocknote/ariakit@2619

@blocknote/code-block

npm i https://pkg.pr.new/@blocknote/code-block@2619

@blocknote/core

npm i https://pkg.pr.new/@blocknote/core@2619

@blocknote/mantine

npm i https://pkg.pr.new/@blocknote/mantine@2619

@blocknote/react

npm i https://pkg.pr.new/@blocknote/react@2619

@blocknote/server-util

npm i https://pkg.pr.new/@blocknote/server-util@2619

@blocknote/shadcn

npm i https://pkg.pr.new/@blocknote/shadcn@2619

@blocknote/xl-ai

npm i https://pkg.pr.new/@blocknote/xl-ai@2619

@blocknote/xl-docx-exporter

npm i https://pkg.pr.new/@blocknote/xl-docx-exporter@2619

@blocknote/xl-email-exporter

npm i https://pkg.pr.new/@blocknote/xl-email-exporter@2619

@blocknote/xl-multi-column

npm i https://pkg.pr.new/@blocknote/xl-multi-column@2619

@blocknote/xl-odt-exporter

npm i https://pkg.pr.new/@blocknote/xl-odt-exporter@2619

@blocknote/xl-pdf-exporter

npm i https://pkg.pr.new/@blocknote/xl-pdf-exporter@2619

commit: c20d1c1

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts (1)

9-9: Consider removing _editor from the signature if external consumers don't depend on it.

Since the hook is exported publicly and the parameter is unused, removal would be a breaking change. If this hook isn't consumed by external packages, dropping the argument would simplify the API and avoid confusion for maintainers.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/react/src/components/SuggestionMenu/GridSuggestionMenu/hooks/useGridSuggestionMenuKeyboardNavigation.ts`:
- Around line 8-15: The hook useGridSuggestionMenuKeyboardNavigation currently
accepts an explicit _editor argument but ignores it by always using
useEditorDOMElement() from context; update the implementation to pass the
explicit editor through to useEditorDOMElement (i.e., call
useEditorDOMElement(editor) or similar) and modify useEditorDOMElement to accept
an optional editorOverride parameter that falls back to useBlockNoteEditor when
not provided so keyboard listeners target the provided editor instance and
restore correct behavior for multi-editor/custom usage.

In `@packages/react/src/hooks/useEditorDomElement.ts`:
- Around line 24-33: The handler passed to editor._tiptapEditor.on("create")
uses setInitialized(true) which is idempotent and prevents re-renders after the
first create; change the effect to update state with a non-idempotent update
(e.g. flip a boolean or increment a counter) so each "create" event forces a
render. Update the useEffect in useEditorDomElement.ts around setInitialized and
the editor._tiptapEditor.on/off("create") handlers to call setInitialized(prev
=> !prev) or setInitialized(n => n + 1) instead of setInitialized(true) so
descendant consumers see a fresh editor.domElement on every create.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c1dc22a0-e78c-431f-8f52-e29b4f4e4561

📥 Commits

Reviewing files that changed from the base of the PR and between 58457e2 and c20d1c1.

📒 Files selected for processing (10)
  • packages/core/src/editor/BlockNoteEditor.ts
  • packages/react/src/components/FormattingToolbar/DefaultButtons/CreateLinkButton.tsx
  • packages/react/src/components/LinkToolbar/LinkToolbarController.tsx
  • packages/react/src/components/Popovers/PositionPopover.tsx
  • packages/react/src/components/SuggestionMenu/GridSuggestionMenu/GridSuggestionMenuController.tsx
  • packages/react/src/components/SuggestionMenu/GridSuggestionMenu/hooks/useGridSuggestionMenuKeyboardNavigation.ts
  • packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx
  • packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts
  • packages/react/src/hooks/useEditorDomElement.ts
  • packages/react/src/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/editor/BlockNoteEditor.ts

Comment on lines +8 to 15
_editor: BlockNoteEditor<any, any, any>,
query: string,
items: Item[],
columns: number,
onItemClick?: (item: Item) => void,
) {
const editorDOMElement = useEditorDOMElement();
const [selectedIndex, setSelectedIndex] = useState<number>(0);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t ignore the explicit editor argument in this exported hook.

The implementation now binds keyboard listeners using context (useEditorDOMElement()), while Line 8 still accepts an editor argument. That can desync targets in multi-editor/custom usage and is a behavioral regression for consumers passing an explicit editor.

💡 Suggested fix
 export function useGridSuggestionMenuKeyboardNavigation<Item>(
-  _editor: BlockNoteEditor<any, any, any>,
+  editor: BlockNoteEditor<any, any, any>,
   query: string,
   items: Item[],
   columns: number,
   onItemClick?: (item: Item) => void,
 ) {
-  const editorDOMElement = useEditorDOMElement();
+  const editorDOMElement = useEditorDOMElement(editor);

And update useEditorDOMElement to accept an optional editor override:

export function useEditorDOMElement(
  editorOverride?: BlockNoteEditor<any, any, any>,
) {
  const contextEditor = useBlockNoteEditor<any, any, any>();
  const editor = editorOverride ?? contextEditor;
  // ...
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/react/src/components/SuggestionMenu/GridSuggestionMenu/hooks/useGridSuggestionMenuKeyboardNavigation.ts`
around lines 8 - 15, The hook useGridSuggestionMenuKeyboardNavigation currently
accepts an explicit _editor argument but ignores it by always using
useEditorDOMElement() from context; update the implementation to pass the
explicit editor through to useEditorDOMElement (i.e., call
useEditorDOMElement(editor) or similar) and modify useEditorDOMElement to accept
an optional editorOverride parameter that falls back to useBlockNoteEditor when
not provided so keyboard listeners target the provided editor instance and
restore correct behavior for multi-editor/custom usage.

Comment on lines +24 to +33
const [, setInitialized] = useState(!editor.headless);
useEffect(() => {
if (!editor.headless) {
return;
}
const handler = () => setInitialized(true);
editor._tiptapEditor.on("create", handler);
return () => {
editor._tiptapEditor.off("create", handler);
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Force a re-render on every create event, not just the first one.

setInitialized(true) is idempotent, so subsequent mount cycles for the same editor instance won’t trigger a render. That can leave descendants bound to stale/missing editor.domElement after remount.

💡 Suggested fix
-  const [, setInitialized] = useState(!editor.headless);
+  const [, bumpMountVersion] = useState(0);
   useEffect(() => {
     if (!editor.headless) {
       return;
     }
-    const handler = () => setInitialized(true);
+    const handler = () => bumpMountVersion((v) => v + 1);
     editor._tiptapEditor.on("create", handler);
     return () => {
       editor._tiptapEditor.off("create", handler);
     };
   }, [editor]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [, setInitialized] = useState(!editor.headless);
useEffect(() => {
if (!editor.headless) {
return;
}
const handler = () => setInitialized(true);
editor._tiptapEditor.on("create", handler);
return () => {
editor._tiptapEditor.off("create", handler);
};
const [, bumpMountVersion] = useState(0);
useEffect(() => {
if (!editor.headless) {
return;
}
const handler = () => bumpMountVersion((v) => v + 1);
editor._tiptapEditor.on("create", handler);
return () => {
editor._tiptapEditor.off("create", handler);
};
}, [editor]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react/src/hooks/useEditorDomElement.ts` around lines 24 - 33, The
handler passed to editor._tiptapEditor.on("create") uses setInitialized(true)
which is idempotent and prevents re-renders after the first create; change the
effect to update state with a non-idempotent update (e.g. flip a boolean or
increment a counter) so each "create" event forces a render. Update the
useEffect in useEditorDomElement.ts around setInitialized and the
editor._tiptapEditor.on/off("create") handlers to call setInitialized(prev =>
!prev) or setInitialized(n => n + 1) instead of setInitialized(true) so
descendant consumers see a fresh editor.domElement on every create.

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.

Link: no menu appears on hover

2 participants