Skip to content

Commit a09f782

Browse files
authored
fix: use stored size in onResizeStop to prevent stale data (#250)
* fix: use stored size in onResizeStop to prevent stale data onResizeStop was recalculating size from props.width/height, but due to React's batched state updates, these props may not have updated yet when onResizeStop fires. This caused the callback to report stale/incorrect size data, particularly noticeable with west/north handles. The fix stores the last calculated size during onResize and uses that stored value in onResizeStop instead of recalculating. Fixes: react-grid-layout/react-grid-layout#2224 * fix: update tests for RTL and stale props fix - Rewrite stale props tests using RTL refs instead of Enzyme - Update onResizeStop test to include onResize call (new behavior) - Fix test expectations for west handle position tracking
1 parent ada8e78 commit a09f782

File tree

2 files changed

+122
-5
lines changed

2 files changed

+122
-5
lines changed

__tests__/Resizable.test.js

Lines changed: 108 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,107 @@ describe('render Resizable', () => {
404404
});
405405
});
406406
});
407+
408+
describe('onResizeStop with stale props', () => {
409+
// This tests the fix for a bug where onResizeStop would report stale size data
410+
// because React's batched state updates mean props.width/height haven't updated yet
411+
// when onResizeStop fires. The fix stores the last size from onResize and uses it
412+
// in onResizeStop. See: https://github.com/react-grid-layout/react-grid-layout/pull/2224
413+
414+
test('onResizeStop reports correct size even when props are stale', () => {
415+
const onResizeStop = jest.fn();
416+
const onResize = jest.fn();
417+
const resizableRef = React.createRef();
418+
render(
419+
<Resizable {...customProps} onResize={onResize} onResizeStop={onResizeStop} ref={resizableRef}>
420+
{resizableBoxChildren}
421+
</Resizable>
422+
);
423+
424+
// Simulate onResizeStart
425+
const startHandler = resizableRef.current.resizeHandler('onResizeStart', 'se');
426+
startHandler(mockEvent, { node, deltaX: 0, deltaY: 0 });
427+
428+
// Simulate dragging - this calls onResize with the new size
429+
const dragHandler = resizableRef.current.resizeHandler('onResize', 'se');
430+
dragHandler(mockEvent, { node, deltaX: 20, deltaY: 30 });
431+
expect(onResize).toHaveBeenLastCalledWith(
432+
mockEvent,
433+
expect.objectContaining({
434+
size: { width: 70, height: 80 },
435+
})
436+
);
437+
438+
// Now simulate onResizeStop. In a real app, React may not have re-rendered yet,
439+
// so props.width/height would still be 50. The deltaX/deltaY from DraggableCore's
440+
// onStop is typically 0 or very small since the mouse hasn't moved since the last
441+
// drag event. Without the fix, this would incorrectly report size: {width: 50, height: 50}.
442+
const stopHandler = resizableRef.current.resizeHandler('onResizeStop', 'se');
443+
stopHandler(mockEvent, { node, deltaX: 0, deltaY: 0 });
444+
445+
// With the fix, onResizeStop should report the same size as the last onResize
446+
expect(onResizeStop).toHaveBeenCalledWith(
447+
mockEvent,
448+
expect.objectContaining({
449+
size: { width: 70, height: 80 },
450+
})
451+
);
452+
});
453+
454+
test('onResizeStop reports correct size for west handle with stale props', () => {
455+
const onResizeStop = jest.fn();
456+
const onResize = jest.fn();
457+
const resizableRef = React.createRef();
458+
const testMockClientRect = { left: 0, top: 0 };
459+
const testNode = document.createElement('div');
460+
testNode.getBoundingClientRect = () => ({ ...testMockClientRect });
461+
462+
render(
463+
<Resizable {...customProps} onResize={onResize} onResizeStop={onResizeStop} ref={resizableRef}>
464+
{resizableBoxChildren}
465+
</Resizable>
466+
);
467+
468+
// Simulate onResizeStart - this sets lastHandleRect to {left: 0, top: 0}
469+
const startHandler = resizableRef.current.resizeHandler('onResizeStart', 'w');
470+
startHandler(mockEvent, { node: testNode, deltaX: 0, deltaY: 0 });
471+
472+
// Simulate dragging west (left)
473+
// deltaX = -15 from drag, plus position adjustment of -15 (handle moved from 0 to -15)
474+
// Total deltaX = -30, reversed for 'w' = +30, so width = 50 + 30 = 80
475+
const dragHandler = resizableRef.current.resizeHandler('onResize', 'w');
476+
testMockClientRect.left = -15;
477+
dragHandler(mockEvent, { node: testNode, deltaX: -15, deltaY: 0 });
478+
expect(onResize).toHaveBeenLastCalledWith(
479+
mockEvent,
480+
expect.objectContaining({
481+
size: { width: 80, height: 50 },
482+
})
483+
);
484+
485+
// Continue dragging - element moves further left
486+
// position adjustment: -25 - (-15) = -10, deltaX becomes -10 + (-10) = -20
487+
// reversed for 'w' = +20, width = 50 + 20 = 70
488+
testMockClientRect.left = -25;
489+
dragHandler(mockEvent, { node: testNode, deltaX: -10, deltaY: 0 });
490+
expect(onResize).toHaveBeenLastCalledWith(
491+
mockEvent,
492+
expect.objectContaining({
493+
size: { width: 70, height: 50 },
494+
})
495+
);
496+
497+
// onResizeStop with stale props - should use stored lastSize (70x50 from last onResize)
498+
const stopHandler = resizableRef.current.resizeHandler('onResizeStop', 'w');
499+
stopHandler(mockEvent, { node: testNode, deltaX: 0, deltaY: 0 });
500+
expect(onResizeStop).toHaveBeenCalledWith(
501+
mockEvent,
502+
expect.objectContaining({
503+
size: { width: 70, height: 50 },
504+
})
505+
);
506+
});
507+
});
407508
});
408509

409510
// ============================================
@@ -588,14 +689,17 @@ describe('render Resizable', () => {
588689
const node = document.createElement('div');
589690
node.getBoundingClientRect = () => ({ left: 0, top: 0 });
590691

591-
// First start
692+
// Start resize
592693
const startHandler = resizableRef.current.resizeHandler('onResizeStart', 'se');
593694
startHandler(mockEvent, { node, deltaX: 0, deltaY: 0 });
594695

595-
// Then stop with a delta - Resizable is stateless so size is calculated from
596-
// props.width/height + delta at time of stop
696+
// Drag to resize - this stores the size for onResizeStop to use
697+
const dragHandler = resizableRef.current.resizeHandler('onResize', 'se');
698+
dragHandler(mockEvent, { node, deltaX: 10, deltaY: 10 });
699+
700+
// Stop resize - uses the stored size from the last onResize
597701
const stopHandler = resizableRef.current.resizeHandler('onResizeStop', 'se');
598-
stopHandler(mockEvent, { node, deltaX: 10, deltaY: 10 });
702+
stopHandler(mockEvent, { node, deltaX: 0, deltaY: 0 });
599703

600704
expect(props.onResizeStop).toHaveBeenCalledWith(
601705
mockEvent,

lib/Resizable.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,14 @@ export default class Resizable extends React.Component<Props, void> {
2424
handleRefs: {[key: ResizeHandleAxis]: ReactRef<HTMLElement>} = {};
2525
lastHandleRect: ?ClientRect = null;
2626
slack: ?[number, number] = null;
27+
lastSize: ?{width: number, height: number} = null;
2728

2829
componentWillUnmount() {
2930
this.resetData();
3031
}
3132

3233
resetData() {
33-
this.lastHandleRect = this.slack = null;
34+
this.lastHandleRect = this.slack = this.lastSize = null;
3435
}
3536

3637
// Clamp width and height within provided constraints
@@ -132,8 +133,20 @@ export default class Resizable extends React.Component<Props, void> {
132133
// Run user-provided constraints.
133134
[width, height] = this.runConstraints(width, height);
134135

136+
// For onResizeStop, use the last size from onResize rather than recalculating.
137+
// This avoids issues where props.width/height are stale due to React's batched updates.
138+
if (handlerName === 'onResizeStop' && this.lastSize) {
139+
({width, height} = this.lastSize);
140+
}
141+
135142
const dimensionsChanged = width !== this.props.width || height !== this.props.height;
136143

144+
// Store the size for use in onResizeStop. We do this after the onResizeStop check
145+
// above so we don't overwrite the stored value with a potentially stale calculation.
146+
if (handlerName !== 'onResizeStop') {
147+
this.lastSize = {width, height};
148+
}
149+
137150
// Call user-supplied callback if present.
138151
const cb = typeof this.props[handlerName] === 'function' ? this.props[handlerName] : null;
139152
// Don't call 'onResize' if dimensions haven't changed.

0 commit comments

Comments
 (0)