Skip to content

fix: use absolute mouse coordinates to eliminate resize drift#270

Open
kajtzu wants to merge 2 commits into
react-grid-layout:masterfrom
kajtzu:fix/absolute-coordinate-resize
Open

fix: use absolute mouse coordinates to eliminate resize drift#270
kajtzu wants to merge 2 commits into
react-grid-layout:masterfrom
kajtzu:fix/absolute-coordinate-resize

Conversation

@kajtzu

@kajtzu kajtzu commented Jul 7, 2026

Copy link
Copy Markdown

Problem

On dashboards with many heavy widgets (maps, topology views, charts), resizing drifts away from the cursor. The handle moves 3-5x slower than the mouse, making it nearly impossible to resize to full width. This is the same core issue reported in #237 and partially addressed in #255.

The lastSize fix from #255 helped but didn't fully solve it. When React can't re-render between consecutive mousemove events (common when renders take 30-50ms due to complex widget trees), multiple events still share a stale base:

Render: props.width = 1443, lastSize.width = 1443
  mousemove A: width = 1443 + 5 = 1448 → lastSize = {width: 1448}
  mousemove B: width = 1448 + 3 = 1451 → lastSize = {width: 1451}  ✓ OK so far
Render: props.width = 1451 (from resizePositionRef feedback)
  mousemove C: width = 1451 + 4 = 1455 → lastSize = {width: 1455}
  ... but props.width was computed from a DIFFERENT path through
      react-grid-layout's onResizeHandler, which may round or constrain
      the value differently, causing lastSize to reset to a wrong base.

The interaction between react-resizable's lastSize tracking and react-grid-layout's resizePositionRef feedback loop creates a situation where the accumulated size oscillates or drifts backwards despite steady mouse movement.

Fix

Replace the incremental delta approach entirely with absolute mouse displacement:

// onResizeStart: capture initial state
this.startSize = { width: this.props.width, height: this.props.height };
this.startMouseX = x;
this.startMouseY = y;

// onResize: compute from absolute displacement
let width = this.startSize.width + (x - this.startMouseX) / transformScale;
let height = this.startSize.height + (y - this.startMouseY) / transformScale;

This has zero accumulation state. The result depends only on:

  1. The initial size at drag start (captured once, never changes)
  2. The current mouse position (always fresh from the DOM event)

No lastSize, no stale props.width, no feedback loops. Mathematically equivalent to a perfect sum of all deltas, but immune to any inter-render timing issues.

Additional change

Removed the per-event getBoundingClientRect() call. It was forcing a synchronous reflow on every mousemove (measured at 40-50ms on our 12-widget dashboard). The handle position correction it provided was only relevant for north/west handles where the element moves during resize — with absolute coordinates this correction is unnecessary for any axis since DraggableCore's x/y are page-relative.

Testing

Tested on a dashboard with 12 widgets (4 map widgets, 4 topology widgets, timeseries, issue manager). Resize now tracks the mouse 1:1 instead of the previous ~30% tracking ratio. Works correctly for all handle axes (se, sw, ne, nw, n, s, e, w).

Summary by CodeRabbit

  • Bug Fixes
    • Improved resize behavior so width and height calculations remain more accurate during dragging.
    • Fixed inconsistent resizing across different drag directions and zoom/scaling settings.
    • Reduced instances where resize callbacks could be skipped or triggered unexpectedly during a resize interaction.

The previous delta-accumulation approach (including the lastSize fix
from react-grid-layout#255 / 3.2.0) still loses movement when React can't re-render
between consecutive mousemove events. On heavy dashboards with 12+
widgets (maps, topology views, etc.), renders take 30-50ms, so 3-8
mouse events pile up per frame. Each event computed width as
baseWidth + deltaX, but baseWidth (from lastSize or props) was stale
for all events after the first in a batch.

This commit replaces the incremental delta approach entirely:

  onResizeStart: record initial widget size + mouse position
  onResize:      width = startWidth + (mouseX - startMouseX)

No accumulation, no stale props, no feedback loops. The calculation
only depends on two things that are always correct: the initial state
(captured once, never changes) and the current mouse position (fresh
from the DOM event).

Also removes the per-event getBoundingClientRect call that forced a
synchronous reflow (40-50ms on complex layouts). It was only needed
for north/west handle position correction, which is unnecessary with
absolute coordinates.

Refs: react-grid-layout#237, react-grid-layout#255
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c159ae97-d5e9-4716-b0aa-bdc21df3479c

📥 Commits

Reviewing files that changed from the base of the PR and between 08281e0 and e42bb90.

📒 Files selected for processing (1)
  • lib/Resizable.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/Resizable.tsx

📝 Walkthrough

Walkthrough

Resizable now records the starting pointer position and size at resize start, then computes dimensions from absolute pointer displacement. The resize callback fallback and no-op suppression logic were updated to use the new tracking state and reference sizes.

Changes

Resizable absolute-coordinate resize refactor

Layer / File(s) Summary
Start-tracking state fields
lib/Resizable.tsx
Adds startSize, startMouseX, startMouseY fields and clears startSize in resetData().
Resize start initialization
lib/Resizable.tsx
Initializes the new tracking state on onResizeStart from current props and callback coordinates after resetting internal state.
Core resize calculation and change detection
lib/Resizable.tsx
Replaces delta/handle-rect resize math with absolute displacement sizing, keeps a fallback branch, and updates dimensionsChanged to compare against lastSize or props.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant DraggableCore
  participant resizeHandler
  participant ResizableState

  User->>DraggableCore: onResizeStart drag
  DraggableCore->>resizeHandler: onResizeStart(mouseX, mouseY)
  resizeHandler->>ResizableState: store startSize, startMouseX, startMouseY
  User->>DraggableCore: drag move
  DraggableCore->>resizeHandler: onResize(mouseX, mouseY)
  resizeHandler->>ResizableState: compute width/height from startSize + displacement
  resizeHandler->>ResizableState: compare against reference size for dimensionsChanged
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main fix: switching resize logic to absolute mouse coordinates to prevent drift.
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.
✨ 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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

Inline comments:
In `@lib/Resizable.tsx`:
- Around line 152-155: The resize dedup logic in Resizable should compare the
current width/height against the previous callback size instead of the drag
origin stored in startSize, since startSize only detects movement from the
initial resize and allows repeated identical onResize calls when dimensions are
clamped or unchanged between events. Update the change check in Resizable.tsx to
use the prior lastSize values available in the resize handler before they are
refreshed, so onResize only fires when the per-event size actually changes.
- Around line 116-134: Keep the position correction logic for north/west resize
handles in Resizable.tsx, since DraggableCore x/y are offsetParent-relative and
the origin can move during resizing. Update the resize calculation in the
handle-delta path so the start-position correction is preserved for axisH ===
'w' and axisV === 'n' instead of relying only on absolute displacement. If you
change the behavior, add or adjust a regression test around Resizable /
DraggableCore with a moving offsetParent to verify n/w resizing stays anchored
correctly.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 92657602-a2c0-45f5-b99a-0efc53c76f82

📥 Commits

Reviewing files that changed from the base of the PR and between 8ca079f and 08281e0.

📒 Files selected for processing (1)
  • lib/Resizable.tsx

Comment thread lib/Resizable.tsx
Comment thread lib/Resizable.tsx Outdated
Comparing against startSize caused redundant onResize callbacks when
the size was clamped at min/max constraints — every event still
differed from the start even though the actual size wasn't changing
between events. Compare against lastSize (the previous callback
value) to properly suppress no-op events.
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