From 9dd7fd993ead0b591e8afc62bf86c220db0a0430 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Tue, 4 Aug 2026 09:50:32 +0200 Subject: [PATCH 1/2] fix(core): guard getBlock() calls to prevent TypeError on stale blocks When a block is removed from the document (via undo, collaboration, or replaceBlocks), closed-over references become stale and getBlock() returns undefined. The non-null assertion (!) let undefined through, causing uncaught TypeErrors in DOM event handlers and React callbacks. Replace all getBlock()! assertions with proper undefined guards across toggle wrappers, side menu, table handles, file panel tabs, file insertion handler, and AI rebase tool. Fixes #2907 --- .../fromClipboard/handleFileInsertion.ts | 11 +++++++++- .../ToggleWrapper/createToggleWrapper.ts | 21 ++++++++++++++----- .../core/src/extensions/SideMenu/SideMenu.ts | 10 ++++++--- .../extensions/TableHandles/TableHandles.ts | 2 +- .../blocks/ToggleWrapper/ToggleWrapper.tsx | 16 ++++++++------ .../FilePanel/DefaultTabs/EmbedTab.tsx | 14 ++++++++----- .../FilePanel/DefaultTabs/UploadTab.tsx | 6 +++++- .../markdown-blocks/tools/rebaseTool.ts | 6 +++++- 8 files changed, 63 insertions(+), 23 deletions(-) diff --git a/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts b/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts index 38c9921f18..aa422bb23a 100644 --- a/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts +++ b/packages/core/src/api/clipboard/fromClipboard/handleFileInsertion.ts @@ -170,9 +170,14 @@ export async function handleFileInsertion< const blockRect = blockElement?.getBoundingClientRect(); + const existingBlock = editor.getBlock(id); + if (!existingBlock) { + return; + } + return insertOrUpdateBlock( editor, - editor.getBlock(id)!, + existingBlock, fileBlock, blockRect && (blockRect.top + blockRect.bottom) / 2 > coords.top ? "before" @@ -183,6 +188,10 @@ export async function handleFileInsertion< return; } + if (!insertedBlockId) { + return; + } + const updateData = await editor.uploadFile(file, insertedBlockId); const updatedFileBlock = diff --git a/packages/core/src/blocks/ToggleWrapper/createToggleWrapper.ts b/packages/core/src/blocks/ToggleWrapper/createToggleWrapper.ts index de2ba427c1..257fd7ce6f 100644 --- a/packages/core/src/blocks/ToggleWrapper/createToggleWrapper.ts +++ b/packages/core/src/blocks/ToggleWrapper/createToggleWrapper.ts @@ -50,20 +50,25 @@ export const createToggleWrapper = ( const toggleButtonOnClick = () => { // Toggles visibility of child blocks. Also adds/removes the "add block" // button if there are no child blocks. + const currentBlock = editor.getBlock(block); + if (!currentBlock) { + return; + } + if (toggleWrapper.getAttribute("data-show-children") === "true") { toggleWrapper.setAttribute("data-show-children", "false"); - toggledState.set(editor.getBlock(block)!, false); + toggledState.set(currentBlock, false); if (dom.contains(toggleAddBlockButton)) { dom.removeChild(toggleAddBlockButton); } } else { toggleWrapper.setAttribute("data-show-children", "true"); - toggledState.set(editor.getBlock(block)!, true); + toggledState.set(currentBlock, true); if ( editor.isEditable && - editor.getBlock(block)?.children.length === 0 && + currentBlock.children.length === 0 && !dom.contains(toggleAddBlockButton) ) { dom.appendChild(toggleAddBlockButton); @@ -111,7 +116,10 @@ export const createToggleWrapper = ( // If a child block is added while children are hidden, show children. if (toggleWrapper.getAttribute("data-show-children") === "false") { toggleWrapper.setAttribute("data-show-children", "true"); - toggledState.set(editor.getBlock(block)!, true); + const currentBlock = editor.getBlock(block); + if (currentBlock) { + toggledState.set(currentBlock, true); + } } // Remove the "add block" button as we want to show child blocks and @@ -124,7 +132,10 @@ export const createToggleWrapper = ( // children. if (toggleWrapper.getAttribute("data-show-children") === "true") { toggleWrapper.setAttribute("data-show-children", "false"); - toggledState.set(editor.getBlock(block)!, false); + const currentBlock = editor.getBlock(block); + if (currentBlock) { + toggledState.set(currentBlock, false); + } } // Remove the "add block" button as we want to hide child blocks, diff --git a/packages/core/src/extensions/SideMenu/SideMenu.ts b/packages/core/src/extensions/SideMenu/SideMenu.ts index e5572c004c..393dd942da 100644 --- a/packages/core/src/extensions/SideMenu/SideMenu.ts +++ b/packages/core/src/extensions/SideMenu/SideMenu.ts @@ -241,6 +241,12 @@ export class SideMenuView< if (this.editor.isEditable) { const blockContentBoundingBox = block.node.getBoundingClientRect(); const column = block.node.closest("[data-node-type=column]"); + const sideMenuBlock = this.editor.getBlock( + this.hoveredBlock!.getAttribute("data-id")!, + ); + if (!sideMenuBlock) { + return; + } this.state = { show: true, referencePos: new DOMRect( @@ -257,9 +263,7 @@ export class SideMenuView< blockContentBoundingBox.width, blockContentBoundingBox.height, ), - block: this.editor.getBlock( - this.hoveredBlock!.getAttribute("data-id")!, - )!, + block: sideMenuBlock, }; this.updateState(this.state); } diff --git a/packages/core/src/extensions/TableHandles/TableHandles.ts b/packages/core/src/extensions/TableHandles/TableHandles.ts index 4616d76b70..2208bc97b5 100644 --- a/packages/core/src/extensions/TableHandles/TableHandles.ts +++ b/packages/core/src/extensions/TableHandles/TableHandles.ts @@ -533,7 +533,7 @@ export class TableHandlesView implements PluginView { } // Hide handles if the table block has been removed. - this.state.block = this.editor.getBlock(this.state.block.id)!; + this.state.block = this.editor.getBlock(this.state.block.id) as any; if ( !this.state.block || this.state.block.type !== "table" || diff --git a/packages/react/src/blocks/ToggleWrapper/ToggleWrapper.tsx b/packages/react/src/blocks/ToggleWrapper/ToggleWrapper.tsx index 7fa0a4bc9a..b35fc8d779 100644 --- a/packages/react/src/blocks/ToggleWrapper/ToggleWrapper.tsx +++ b/packages/react/src/blocks/ToggleWrapper/ToggleWrapper.tsx @@ -58,10 +58,11 @@ export const ToggleWrapper = ( ); const handleToggle = (block: Block) => { - (toggledState || defaultToggledState).set( - editor.getBlock(block)!, - !showChildren, - ); + const currentBlock = editor.getBlock(block); + if (!currentBlock) { + return; + } + (toggledState || defaultToggledState).set(currentBlock, !showChildren); dispatch({ type: "toggled", }); @@ -91,7 +92,10 @@ export const ToggleWrapper = ( return 0; } - const newBlock = editor.getBlock(block)!; + const newBlock = editor.getBlock(block); + if (!newBlock) { + return 0; + } const newChildCount = newBlock.children.length || 0; if (newChildCount > childCount) { @@ -122,7 +126,7 @@ export const ToggleWrapper = ( className="bn-toggle-button" type="button" onMouseDown={(event) => event.preventDefault()} - onClick={() => handleToggle(editor.getBlock(block)!)} + onClick={() => handleToggle(block)} > (); - const block = editor.getBlock(props.blockId)!; + const block = editor.getBlock(props.blockId); const [currentURL, setCurrentURL] = useState(""); @@ -41,7 +41,7 @@ export const EmbedTab = < (event: KeyboardEvent) => { if (event.key === "Enter" && !event.nativeEvent.isComposing) { event.preventDefault(); - editor.updateBlock(block.id, { + editor.updateBlock(props.blockId, { props: { name: filenameFromURL(currentURL), url: currentURL, @@ -49,17 +49,21 @@ export const EmbedTab = < }); } }, - [editor, block.id, currentURL], + [editor, props.blockId, currentURL], ); const handleURLClick = useCallback(() => { - editor.updateBlock(block.id, { + editor.updateBlock(props.blockId, { props: { name: filenameFromURL(currentURL), url: currentURL, } as any, }); - }, [editor, block.id, currentURL]); + }, [editor, props.blockId, currentURL]); + + if (!block) { + return null; + } return ( diff --git a/packages/react/src/components/FilePanel/DefaultTabs/UploadTab.tsx b/packages/react/src/components/FilePanel/DefaultTabs/UploadTab.tsx index fa8a5a2d49..1319700d01 100644 --- a/packages/react/src/components/FilePanel/DefaultTabs/UploadTab.tsx +++ b/packages/react/src/components/FilePanel/DefaultTabs/UploadTab.tsx @@ -29,7 +29,7 @@ export const UploadTab = < const editor = useBlockNoteEditor(); - const block = editor.getBlock(props.blockId)!; + const block = editor.getBlock(props.blockId); const [uploadFailed, setUploadFailed] = useState(false); @@ -76,6 +76,10 @@ export const UploadTab = < [props.blockId, editor, setLoading], ); + if (!block) { + return null; + } + const spec = editor.schema.blockSpecs[block.type]; const accept = spec.implementation.meta?.fileBlockAccept?.length ? spec.implementation.meta.fileBlockAccept.join(",") diff --git a/packages/xl-ai/src/api/formats/markdown-blocks/tools/rebaseTool.ts b/packages/xl-ai/src/api/formats/markdown-blocks/tools/rebaseTool.ts index aff89b423b..2e4b31136f 100644 --- a/packages/xl-ai/src/api/formats/markdown-blocks/tools/rebaseTool.ts +++ b/packages/xl-ai/src/api/formats/markdown-blocks/tools/rebaseTool.ts @@ -11,7 +11,11 @@ export function createMDRebaseTool( editor: BlockNoteEditor, ) { const tr = getApplySuggestionsTr(editor); - const md = editor.blocksToMarkdownLossy([getBlock(tr.doc, id)!]); + const block = getBlock(tr.doc, id); + if (!block) { + throw new Error("block not found"); + } + const md = editor.blocksToMarkdownLossy([block]); const blocks = editor.tryParseMarkdownToBlocks(md); const steps = updateToReplaceSteps( From 12e11aa1121ed7628875e6f7fc9a0478a3025604 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Tue, 4 Aug 2026 10:13:55 +0200 Subject: [PATCH 2/2] fix: improve getBlock() guards with proper state cleanup and async safety - SideMenu: hide menu and clear hoveredBlock when getBlock returns undefined, preventing stale visible state - EmbedTab: re-check block existence before each updateBlock call - UploadTab: re-check block existence after async upload resolves - TableHandles: use typed local variable instead of as any cast --- packages/core/src/extensions/SideMenu/SideMenu.ts | 5 +++++ packages/core/src/extensions/TableHandles/TableHandles.ts | 7 ++++--- .../src/components/FilePanel/DefaultTabs/EmbedTab.tsx | 6 ++++++ .../src/components/FilePanel/DefaultTabs/UploadTab.tsx | 3 +++ 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/core/src/extensions/SideMenu/SideMenu.ts b/packages/core/src/extensions/SideMenu/SideMenu.ts index 393dd942da..d53a8444cc 100644 --- a/packages/core/src/extensions/SideMenu/SideMenu.ts +++ b/packages/core/src/extensions/SideMenu/SideMenu.ts @@ -245,6 +245,11 @@ export class SideMenuView< this.hoveredBlock!.getAttribute("data-id")!, ); if (!sideMenuBlock) { + if (this.state?.show) { + this.state.show = false; + this.hoveredBlock = undefined; + this.emitUpdate(this.state); + } return; } this.state = { diff --git a/packages/core/src/extensions/TableHandles/TableHandles.ts b/packages/core/src/extensions/TableHandles/TableHandles.ts index 2208bc97b5..4a019aa3db 100644 --- a/packages/core/src/extensions/TableHandles/TableHandles.ts +++ b/packages/core/src/extensions/TableHandles/TableHandles.ts @@ -533,10 +533,10 @@ export class TableHandlesView implements PluginView { } // Hide handles if the table block has been removed. - this.state.block = this.editor.getBlock(this.state.block.id) as any; + const refreshedBlock = this.editor.getBlock(this.state.block.id); if ( - !this.state.block || - this.state.block.type !== "table" || + !refreshedBlock || + refreshedBlock.type !== "table" || // when collaborating, the table element might be replaced and out of date // because yjs replaces the element when for example you change the color via the side menu !this.tableElement?.isConnected @@ -548,6 +548,7 @@ export class TableHandlesView implements PluginView { return; } + this.state.block = refreshedBlock as typeof this.state.block; const { height: rowCount, width: colCount } = getDimensionsOfTable( this.state.block, diff --git a/packages/react/src/components/FilePanel/DefaultTabs/EmbedTab.tsx b/packages/react/src/components/FilePanel/DefaultTabs/EmbedTab.tsx index 5b2e6b64ea..9c824ba8bf 100644 --- a/packages/react/src/components/FilePanel/DefaultTabs/EmbedTab.tsx +++ b/packages/react/src/components/FilePanel/DefaultTabs/EmbedTab.tsx @@ -41,6 +41,9 @@ export const EmbedTab = < (event: KeyboardEvent) => { if (event.key === "Enter" && !event.nativeEvent.isComposing) { event.preventDefault(); + if (!editor.getBlock(props.blockId)) { + return; + } editor.updateBlock(props.blockId, { props: { name: filenameFromURL(currentURL), @@ -53,6 +56,9 @@ export const EmbedTab = < ); const handleURLClick = useCallback(() => { + if (!editor.getBlock(props.blockId)) { + return; + } editor.updateBlock(props.blockId, { props: { name: filenameFromURL(currentURL), diff --git a/packages/react/src/components/FilePanel/DefaultTabs/UploadTab.tsx b/packages/react/src/components/FilePanel/DefaultTabs/UploadTab.tsx index 1319700d01..41e726ca72 100644 --- a/packages/react/src/components/FilePanel/DefaultTabs/UploadTab.tsx +++ b/packages/react/src/components/FilePanel/DefaultTabs/UploadTab.tsx @@ -53,6 +53,9 @@ export const UploadTab = < if (editor.uploadFile !== undefined) { try { let updateData = await editor.uploadFile(file, props.blockId); + if (!editor.getBlock(props.blockId)) { + return; + } if (typeof updateData === "string") { // received a url updateData = {