Skip to content

Commit 03ef56b

Browse files
committed
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
1 parent 070d0ad commit 03ef56b

File tree

2 files changed

+111
-1
lines changed

2 files changed

+111
-1
lines changed

__tests__/Resizable.test.js

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,5 +306,102 @@ describe('render Resizable', () => {
306306
});
307307
});
308308
});
309+
310+
describe('onResizeStop with stale props', () => {
311+
// This tests the fix for a bug where onResizeStop would report stale size data
312+
// because React's batched state updates mean props.width/height haven't updated yet
313+
// when onResizeStop fires. The fix stores the last size from onResize and uses it
314+
// in onResizeStop. See: https://github.com/react-grid-layout/react-grid-layout/pull/2224
315+
316+
test('onResizeStop reports correct size even when props are stale', () => {
317+
// Create a fresh element with fresh mocks for this test
318+
const onResizeStop = jest.fn();
319+
const onResize = jest.fn();
320+
const testProps = {
321+
...customProps,
322+
onResize,
323+
onResizeStop,
324+
};
325+
const element = shallow(<Resizable {...testProps}>{resizableBoxChildren}</Resizable>);
326+
const seHandle = findHandle(element, 'se');
327+
328+
// Simulate onResizeStart
329+
seHandle.prop('onStart')(mockEvent, { node, deltaX: 0, deltaY: 0 });
330+
331+
// Simulate dragging - this calls onResize with the new size
332+
seHandle.prop('onDrag')(mockEvent, { node, deltaX: 20, deltaY: 30 });
333+
expect(onResize).toHaveBeenLastCalledWith(
334+
mockEvent,
335+
expect.objectContaining({
336+
size: { width: 70, height: 80 },
337+
})
338+
);
339+
340+
// Now simulate onResizeStop. In a real app, React may not have re-rendered yet,
341+
// so props.width/height would still be 50. The deltaX/deltaY from DraggableCore's
342+
// onStop is typically 0 or very small since the mouse hasn't moved since the last
343+
// drag event. Without the fix, this would incorrectly report size: {width: 50, height: 50}.
344+
seHandle.prop('onStop')(mockEvent, { node, deltaX: 0, deltaY: 0 });
345+
346+
// With the fix, onResizeStop should report the same size as the last onResize
347+
expect(onResizeStop).toHaveBeenCalledWith(
348+
mockEvent,
349+
expect.objectContaining({
350+
size: { width: 70, height: 80 },
351+
})
352+
);
353+
});
354+
355+
test('onResizeStop reports correct size for west handle with stale props', () => {
356+
const onResizeStop = jest.fn();
357+
const onResize = jest.fn();
358+
const testProps = {
359+
...customProps,
360+
onResize,
361+
onResizeStop,
362+
};
363+
const testMockClientRect = { left: 0, top: 0 };
364+
const testNode = document.createElement('div');
365+
// $FlowIgnore
366+
testNode.getBoundingClientRect = () => ({ ...testMockClientRect });
367+
368+
const element = shallow(<Resizable {...testProps}>{resizableBoxChildren}</Resizable>);
369+
const wHandle = findHandle(element, 'w');
370+
371+
// Simulate onResizeStart - this sets lastHandleRect to {left: 0, top: 0}
372+
wHandle.prop('onStart')(mockEvent, { node: testNode, deltaX: 0, deltaY: 0 });
373+
374+
// Simulate dragging west (left)
375+
// deltaX = -15 from drag, plus position adjustment of -15 (handle moved from 0 to -15)
376+
// Total deltaX = -30, reversed for 'w' = +30, so width = 50 + 30 = 80
377+
testMockClientRect.left = -15;
378+
wHandle.prop('onDrag')(mockEvent, { node: testNode, deltaX: -15, deltaY: 0 });
379+
expect(onResize).toHaveBeenLastCalledWith(
380+
mockEvent,
381+
expect.objectContaining({
382+
size: { width: 80, height: 50 },
383+
})
384+
);
385+
386+
// Continue dragging - element moves further left
387+
testMockClientRect.left = -25;
388+
wHandle.prop('onDrag')(mockEvent, { node: testNode, deltaX: -10, deltaY: 0 });
389+
expect(onResize).toHaveBeenLastCalledWith(
390+
mockEvent,
391+
expect.objectContaining({
392+
size: { width: 100, height: 50 },
393+
})
394+
);
395+
396+
// onResizeStop with stale props - should use stored lastSize
397+
wHandle.prop('onStop')(mockEvent, { node: testNode, deltaX: 0, deltaY: 0 });
398+
expect(onResizeStop).toHaveBeenCalledWith(
399+
mockEvent,
400+
expect.objectContaining({
401+
size: { width: 100, height: 50 },
402+
})
403+
);
404+
});
405+
});
309406
});
310407
});

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)