Skip to content

fix(TableHandles): guard stale block state after doc updates#2821

Open
satoren wants to merge 1 commit into
TypeCellOS:mainfrom
satoren:fix/table-handles-stale-block-state
Open

fix(TableHandles): guard stale block state after doc updates#2821
satoren wants to merge 1 commit into
TypeCellOS:mainfrom
satoren:fix/table-handles-stale-block-state

Conversation

@satoren
Copy link
Copy Markdown

@satoren satoren commented Jun 4, 2026

Summary

Fixes a TypeError: Cannot read properties of undefined (reading 'id') in TableHandlesView.mouseMoveHandler when hovering a table wrapper after the underlying table block was removed or became invalid (e.g. collaborative edits / Yjs DOM replacement).

Root cause

  1. update() refreshes this.state.block via getBlock(id). When the block is gone, block becomes undefined, but the PluginView kept the state object (only show was set to false).

  2. On a later mousemove over the table wrapper, this expression ran before new state was assigned:

    this.state?.block.id !== tableBlock.id

    Because optional chaining stops at block, (this.state?.block).id throws when block is undefined.

Fix

  • Use this.state?.block?.id in the wrapper hover branch.
  • When the table block is invalid in update(), clear internal PluginView state (this.state, tableId, tableElement) instead of leaving a stale object.
  • Allow emitUpdate to pass undefined to the extension store (matching existing state.block ? … : undefined logic).

Reproduction (consumer apps)

  1. Open an editor with collaboration enabled and a table in the document.
  2. Hover the table so table handles state is initialized.
  3. Remove or replace the table block remotely (or disconnect the table DOM).
  4. Move the mouse to the table wrapper area (below/right of the table).

Reported in

  • ovice-ui Sentry: OVICE-UI-15SZ (@blocknote/core 0.51, staging, mouseMoveHandler)

Made with Cursor

Summary by CodeRabbit

  • Bug Fixes
    • Fixed crashes during table handle initialization and updates
    • Resolved crashes in table block hover detection logic
    • Improved state cleanup when tables become invalid
    • Enhanced stability when interacting with table blocks

Optional chaining on `this.state?.block.id` did not protect when `block`
was undefined after `update()` refreshed a removed table block. Clear
internal PluginView state instead of only hiding handles, and allow
emitUpdate to sync undefined state to the extension store.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 4, 2026

@satoren is attempting to deploy a commit to the TypeCell Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 4, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

The TableHandlesView plugin in BlockNote is updated to safely handle undefined state throughout its lifecycle. The constructor callback type is widened to accept undefined, state access is guarded with optional chaining in comparisons and initialization, and the update logic now fully clears internal tracking when a table becomes disconnected or invalid.

Changes

Table Handles Robustness

Layer / File(s) Summary
Constructor callback and store initialization safety
packages/core/src/extensions/TableHandles/TableHandles.ts
The emitUpdate callback parameter type is widened to `TableHandlesState
Runtime state comparisons and cleanup
packages/core/src/extensions/TableHandles/TableHandles.ts
The hover/visibility logic safely compares block IDs using optional chaining (this.state?.block?.id). The update() method now fully resets internal tracking (this.state, this.tableId, this.tableElement) when the underlying table is no longer valid, instead of only toggling visibility flags.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Suggested reviewers

  • nperez0111

Poem

🐰 Handles without a table to hold,
Now gracefully fade instead of scold;
Optional chains keep crashes at bay,
StateUp cleared when tables stray,
Robustness blooms in small edits today! 🌱

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and concisely describes the main bug fix: guarding against stale block state that causes crashes after document updates during table hover operations.
Description check ✅ Passed The description provides a thorough summary, clear root cause explanation, detailed fix strategy, and reproduction steps. While some template sections (like Testing and Checklist) are not filled, the core content is comprehensive and addresses the PR's purpose well.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/core/src/extensions/TableHandles/TableHandles.ts (2)

303-316: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Pre-existing issue: Non-null assertion could crash on first wrapper hover.

Line 304 uses ...this.state! which assumes this.state is always defined when hovering the table wrapper. However, if a user hovers the wrapper before hovering any cell, this.state will be undefined (initialized at line 146), causing a runtime error.

This is inconsistent with the defensive approach taken in this PR (optional chaining at line 297, nullable type at line 166, clearing state at lines 542-544).

🛡️ Proposed fix to use optional spread
     this.state = {
-      ...this.state!,
+      ...(this.state ?? {}),
       show: true,
       showAddOrRemoveRowsButton: belowTable,

This ensures the spread operation safely handles undefined state by defaulting to an empty object, aligning with the defensive state handling throughout the rest of this PR.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/extensions/TableHandles/TableHandles.ts` around lines 303 -
316, The assignment currently uses a non-null assertion "...this.state!" which
can throw if this.state is undefined; change the spread to safely handle
undefined by using a default object (e.g. "...(this.state ?? {})") when building
the new state so components like showAddOrRemoveRowsButton,
showAddOrRemoveColumnsButton, referencePosTable, block, widgetContainer,
colIndex, rowIndex and referencePosCell are merged without assuming this.state
exists.

534-534: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Pre-existing issue: Non-null assertion contradicts subsequent check.

Line 534 uses a non-null assertion (!) on getBlock(), telling TypeScript the result is never undefined. However, line 536 immediately checks if (!this.state.block ...), indicating the code expects it might be undefined.

This creates a disconnect between the type system and runtime behavior. While not a runtime bug (the check at line 536 handles it), it reduces type safety and is inconsistent with the defensive approach of this PR.

♻️ Proposed fix to remove misleading assertion
-    this.state.block = this.editor.getBlock(this.state.block.id)!;
+    this.state.block = this.editor.getBlock(this.state.block.id);

This aligns the type signature with the runtime check and improves consistency with the defensive state handling added elsewhere in this PR.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/extensions/TableHandles/TableHandles.ts` at line 534,
Remove the misleading non-null assertion on the getBlock call in TableHandles so
the assignment reflects that the result may be undefined: change the assignment
using this.editor.getBlock(this.state.block.id)! to one without the "!"
(this.editor.getBlock(this.state.block.id)) and keep the existing defensive
check if (!this.state.block ...) that follows; ensure any downstream usage of
this.state.block handles the undefined case or narrows the type after the check
so TypeScript no longer believes the value is always non-null.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/core/src/extensions/TableHandles/TableHandles.ts`:
- Around line 303-316: The assignment currently uses a non-null assertion
"...this.state!" which can throw if this.state is undefined; change the spread
to safely handle undefined by using a default object (e.g. "...(this.state ??
{})") when building the new state so components like showAddOrRemoveRowsButton,
showAddOrRemoveColumnsButton, referencePosTable, block, widgetContainer,
colIndex, rowIndex and referencePosCell are merged without assuming this.state
exists.
- Line 534: Remove the misleading non-null assertion on the getBlock call in
TableHandles so the assignment reflects that the result may be undefined: change
the assignment using this.editor.getBlock(this.state.block.id)! to one without
the "!" (this.editor.getBlock(this.state.block.id)) and keep the existing
defensive check if (!this.state.block ...) that follows; ensure any downstream
usage of this.state.block handles the undefined case or narrows the type after
the check so TypeScript no longer believes the value is always non-null.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5563fdc9-4a45-4a15-b182-a8577b2c99d0

📥 Commits

Reviewing files that changed from the base of the PR and between ec9c151 and 6fc29db.

📒 Files selected for processing (1)
  • packages/core/src/extensions/TableHandles/TableHandles.ts

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.

1 participant