Skip to content

Conversation

narraje
Copy link
Contributor

@narraje narraje commented Sep 23, 2025

Fix: Resolve infinite loop in useFloatingToolbar hook (v2)

Problem

The floating toolbar was causing infinite re-renders under certain conditions, leading to performance issues and potential browser hangs. This occurred when users interacted with text selections while the toolbar was visible.

Additionally, an initial attempt to fix this issue by simply removing the open dependency broke critical button-click functionality, making toolbar buttons unclickable.


Root Cause Analysis

The third useEffect in useFloatingToolbar had open in its dependency array while also calling setOpen inside the effect, creating a feedback loop:

**// Problematic code - creates infinite loop**
React.useEffect(() => {
  if (
    !selectionExpanded ||
    !selectionText ||
    (mousedown && !open) ||  // ← Reads 'open'
    hideToolbar ||
    (readOnly && !showWhenReadOnly)
  ) {
    setOpen(false);  // ← Writes 'open'
  } else if (
    selectionText &&
    selectionExpanded &&
    (!waitForCollapsedSelection || readOnly)
  ) {
    setOpen(true);   // ← Writes 'open'
  }
}, [
  // ... other deps
  open,  // ← Creates feedback loop: setOpen → open changes → effect re-runs → setOpen
  // ...
]);

Why the first fix failed: Simply removing open from the dependency array broke the (mousedown && !open) condition, which prevents a race condition during button clicks. When a user clicks a toolbar button:

  • mousedown
  • event fires → editor loses focus
  • Toolbar tries to close immediately
  • Button disappears before click event can complete
  • Result: unclickable buttons

The (mousedown && !open) condition was specifically designed to prevent closing the toolbar during mousedown when it's already open, allowing click events to complete.


Solution

Use the functional setState pattern to access current state without creating a dependency cycle:

typescript
// Fixed code - no infinite loop, preserves functionality
React.useEffect(() => {
  setOpen((prevOpen: boolean) => {
    if (
      !selectionExpanded ||
      !selectionText ||
      (mousedown && !prevOpen) ||  // ← Uses prevOpen instead of stale closure value
      hideToolbar ||
      (readOnly && !showWhenReadOnly)
    ) {
      return false;
    } else if (
      selectionText &&
      selectionExpanded &&
      (!waitForCollapsedSelection || readOnly)
    ) {
      return true;
    }
    return prevOpen; // No change needed
  });
}, [
  setOpen,
  editorId,
  focusedEditorId,
  hideToolbar,
  showWhenReadOnly,
  selectionExpanded,
  selectionText,
  mousedown,
  waitForCollapsedSelection,
  readOnly,
  // ← No 'open' dependency - eliminates infinite loop!
]);

Why This Works

✅ Eliminates infinite loop: No open in dependency array
✅ Preserves button functionality: Uses prevOpen to maintain event timing logic
✅ Same behavior: Toolbar opens/closes exactly as before
✅ Performance: No more endless re-renders

Files Changed

packages/floating/src/hooks/useFloatingToolbar.ts

This solution demonstrates the power of React's functional setState pattern for resolving dependency cycles while preserving critical component behavior.

Copy link

codesandbox bot commented Sep 23, 2025

Review or Edit in CodeSandbox

Open the branch in Web EditorVS CodeInsiders

Open Preview

Copy link

changeset-bot bot commented Sep 23, 2025

🦋 Changeset detected

Latest commit: 323593d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@platejs/floating Patch
@platejs/link Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@dosubot dosubot bot added size:M This PR changes 30-99 lines, ignoring generated files. bug Something isn't working labels Sep 23, 2025
Copy link

vercel bot commented Sep 23, 2025

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

Project Deployment Preview Comments Updated (UTC)
plate Ready Ready Preview Comment Oct 3, 2025 3:14pm

@zbeyens zbeyens marked this pull request as draft September 23, 2025 19:30
@narraje narraje marked this pull request as ready for review September 24, 2025 20:44
@dosubot dosubot bot added the patch Bugfix & documentation PR label Sep 24, 2025
@zbeyens zbeyens merged commit 030890f into udecode:main Oct 3, 2025
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working patch Bugfix & documentation PR size:M This PR changes 30-99 lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants