diff --git a/__tests__/Resizable.test.js b/__tests__/Resizable.test.js
index 2107cc26..9ad2c71a 100644
--- a/__tests__/Resizable.test.js
+++ b/__tests__/Resizable.test.js
@@ -404,6 +404,107 @@ describe('render Resizable', () => {
});
});
});
+
+ describe('onResizeStop with stale props', () => {
+ // This tests the fix for a bug where onResizeStop would report stale size data
+ // because React's batched state updates mean props.width/height haven't updated yet
+ // when onResizeStop fires. The fix stores the last size from onResize and uses it
+ // in onResizeStop. See: https://github.com/react-grid-layout/react-grid-layout/pull/2224
+
+ test('onResizeStop reports correct size even when props are stale', () => {
+ const onResizeStop = jest.fn();
+ const onResize = jest.fn();
+ const resizableRef = React.createRef();
+ render(
+
+ {resizableBoxChildren}
+
+ );
+
+ // Simulate onResizeStart
+ const startHandler = resizableRef.current.resizeHandler('onResizeStart', 'se');
+ startHandler(mockEvent, { node, deltaX: 0, deltaY: 0 });
+
+ // Simulate dragging - this calls onResize with the new size
+ const dragHandler = resizableRef.current.resizeHandler('onResize', 'se');
+ dragHandler(mockEvent, { node, deltaX: 20, deltaY: 30 });
+ expect(onResize).toHaveBeenLastCalledWith(
+ mockEvent,
+ expect.objectContaining({
+ size: { width: 70, height: 80 },
+ })
+ );
+
+ // Now simulate onResizeStop. In a real app, React may not have re-rendered yet,
+ // so props.width/height would still be 50. The deltaX/deltaY from DraggableCore's
+ // onStop is typically 0 or very small since the mouse hasn't moved since the last
+ // drag event. Without the fix, this would incorrectly report size: {width: 50, height: 50}.
+ const stopHandler = resizableRef.current.resizeHandler('onResizeStop', 'se');
+ stopHandler(mockEvent, { node, deltaX: 0, deltaY: 0 });
+
+ // With the fix, onResizeStop should report the same size as the last onResize
+ expect(onResizeStop).toHaveBeenCalledWith(
+ mockEvent,
+ expect.objectContaining({
+ size: { width: 70, height: 80 },
+ })
+ );
+ });
+
+ test('onResizeStop reports correct size for west handle with stale props', () => {
+ const onResizeStop = jest.fn();
+ const onResize = jest.fn();
+ const resizableRef = React.createRef();
+ const testMockClientRect = { left: 0, top: 0 };
+ const testNode = document.createElement('div');
+ testNode.getBoundingClientRect = () => ({ ...testMockClientRect });
+
+ render(
+
+ {resizableBoxChildren}
+
+ );
+
+ // Simulate onResizeStart - this sets lastHandleRect to {left: 0, top: 0}
+ const startHandler = resizableRef.current.resizeHandler('onResizeStart', 'w');
+ startHandler(mockEvent, { node: testNode, deltaX: 0, deltaY: 0 });
+
+ // Simulate dragging west (left)
+ // deltaX = -15 from drag, plus position adjustment of -15 (handle moved from 0 to -15)
+ // Total deltaX = -30, reversed for 'w' = +30, so width = 50 + 30 = 80
+ const dragHandler = resizableRef.current.resizeHandler('onResize', 'w');
+ testMockClientRect.left = -15;
+ dragHandler(mockEvent, { node: testNode, deltaX: -15, deltaY: 0 });
+ expect(onResize).toHaveBeenLastCalledWith(
+ mockEvent,
+ expect.objectContaining({
+ size: { width: 80, height: 50 },
+ })
+ );
+
+ // Continue dragging - element moves further left
+ // position adjustment: -25 - (-15) = -10, deltaX becomes -10 + (-10) = -20
+ // reversed for 'w' = +20, width = 50 + 20 = 70
+ testMockClientRect.left = -25;
+ dragHandler(mockEvent, { node: testNode, deltaX: -10, deltaY: 0 });
+ expect(onResize).toHaveBeenLastCalledWith(
+ mockEvent,
+ expect.objectContaining({
+ size: { width: 70, height: 50 },
+ })
+ );
+
+ // onResizeStop with stale props - should use stored lastSize (70x50 from last onResize)
+ const stopHandler = resizableRef.current.resizeHandler('onResizeStop', 'w');
+ stopHandler(mockEvent, { node: testNode, deltaX: 0, deltaY: 0 });
+ expect(onResizeStop).toHaveBeenCalledWith(
+ mockEvent,
+ expect.objectContaining({
+ size: { width: 70, height: 50 },
+ })
+ );
+ });
+ });
});
// ============================================
@@ -588,14 +689,17 @@ describe('render Resizable', () => {
const node = document.createElement('div');
node.getBoundingClientRect = () => ({ left: 0, top: 0 });
- // First start
+ // Start resize
const startHandler = resizableRef.current.resizeHandler('onResizeStart', 'se');
startHandler(mockEvent, { node, deltaX: 0, deltaY: 0 });
- // Then stop with a delta - Resizable is stateless so size is calculated from
- // props.width/height + delta at time of stop
+ // Drag to resize - this stores the size for onResizeStop to use
+ const dragHandler = resizableRef.current.resizeHandler('onResize', 'se');
+ dragHandler(mockEvent, { node, deltaX: 10, deltaY: 10 });
+
+ // Stop resize - uses the stored size from the last onResize
const stopHandler = resizableRef.current.resizeHandler('onResizeStop', 'se');
- stopHandler(mockEvent, { node, deltaX: 10, deltaY: 10 });
+ stopHandler(mockEvent, { node, deltaX: 0, deltaY: 0 });
expect(props.onResizeStop).toHaveBeenCalledWith(
mockEvent,
diff --git a/lib/Resizable.js b/lib/Resizable.js
index a2eede3d..fb62f8cf 100644
--- a/lib/Resizable.js
+++ b/lib/Resizable.js
@@ -24,13 +24,14 @@ export default class Resizable extends React.Component {
handleRefs: {[key: ResizeHandleAxis]: ReactRef} = {};
lastHandleRect: ?ClientRect = null;
slack: ?[number, number] = null;
+ lastSize: ?{width: number, height: number} = null;
componentWillUnmount() {
this.resetData();
}
resetData() {
- this.lastHandleRect = this.slack = null;
+ this.lastHandleRect = this.slack = this.lastSize = null;
}
// Clamp width and height within provided constraints
@@ -132,8 +133,20 @@ export default class Resizable extends React.Component {
// Run user-provided constraints.
[width, height] = this.runConstraints(width, height);
+ // For onResizeStop, use the last size from onResize rather than recalculating.
+ // This avoids issues where props.width/height are stale due to React's batched updates.
+ if (handlerName === 'onResizeStop' && this.lastSize) {
+ ({width, height} = this.lastSize);
+ }
+
const dimensionsChanged = width !== this.props.width || height !== this.props.height;
+ // Store the size for use in onResizeStop. We do this after the onResizeStop check
+ // above so we don't overwrite the stored value with a potentially stale calculation.
+ if (handlerName !== 'onResizeStop') {
+ this.lastSize = {width, height};
+ }
+
// Call user-supplied callback if present.
const cb = typeof this.props[handlerName] === 'function' ? this.props[handlerName] : null;
// Don't call 'onResize' if dimensions haven't changed.