Skip to content

Commit b813f22

Browse files
authored
fix(ResizablePanel): infinite loop in controllable mode (#543)
1 parent d22e020 commit b813f22

File tree

3 files changed

+41
-21
lines changed

3 files changed

+41
-21
lines changed

.changeset/smooth-donuts-exist.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@cube-dev/ui-kit': patch
3+
---
4+
5+
Prevent ResizablePanel from infinite switching state loop in controllable mode.

src/components/layout/ResizablePanel.stories.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ const TemplateTop: StoryFn<CubeResizablePanelProps> = (args) => {
4444

4545
const TemplateControllable: StoryFn<CubeResizablePanelProps> = (args) => {
4646
const [size, setSize] = useState(200);
47-
4847
return (
4948
<ResizablePanel
5049
size={size}

src/components/layout/ResizablePanel.tsx

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { ForwardedRef, forwardRef, useEffect, useMemo, useState } from 'react';
22
import { useHover, useMove } from 'react-aria';
33

44
import { BasePropsWithoutChildren, Styles, tasty } from '../../tasty/index';
5-
import { mergeProps, useCombinedRefs } from '../../utils/react/index';
5+
import { mergeProps, useCombinedRefs } from '../../utils/react';
6+
import { useEvent } from '../../_internal/hooks';
67

78
import { Panel, CubePanelProps } from './Panel';
89

@@ -217,19 +218,6 @@ function ResizablePanel(
217218
);
218219
let [visualSize, setVisualSize] = useState<number | null>(null);
219220

220-
useEffect(() => {
221-
if (ref.current) {
222-
const offsetProp = isHorizontal ? 'offsetWidth' : 'offsetHeight';
223-
const containerSize = ref.current[offsetProp];
224-
225-
if (Math.abs(containerSize - size) < 1 && !isDisabled) {
226-
setVisualSize(size);
227-
} else {
228-
setVisualSize(containerSize);
229-
}
230-
}
231-
}, [size, isDisabled]);
232-
233221
let { moveProps } = useMove({
234222
onMoveStart(e) {
235223
if (isDisabled) {
@@ -258,7 +246,7 @@ function ResizablePanel(
258246
? e.deltaX * (direction === 'right' ? 1 : -1)
259247
: e.deltaY * (direction === 'bottom' ? 1 : -1);
260248

261-
return clamp(size);
249+
return size;
262250
});
263251
},
264252
onMoveEnd(e) {
@@ -267,16 +255,44 @@ function ResizablePanel(
267255
},
268256
});
269257

258+
// Since we sync provided size and the local one in two ways
259+
// we need a way to prevent infinite loop in some cases.
260+
// We will run this in setTimeout and make sure it will get the most recent state.
261+
const notifyChange = useEvent(() => {
262+
setSize((size) => {
263+
if (providedSize && Math.abs(providedSize - size) > 0.5) {
264+
return providedSize;
265+
}
266+
267+
return size;
268+
});
269+
});
270+
270271
useEffect(() => {
271-
if (providedSize == null || Math.abs(providedSize - size) > 0.5) {
272-
onSizeChange?.(Math.round(size));
272+
if (ref.current) {
273+
const offsetProp = isHorizontal ? 'offsetWidth' : 'offsetHeight';
274+
const containerSize = ref.current[offsetProp];
275+
276+
if (Math.abs(containerSize - size) < 1 && !isDisabled) {
277+
setVisualSize(size);
278+
} else {
279+
setVisualSize(containerSize);
280+
}
273281
}
274-
}, [size]);
282+
}, [size, isDisabled]);
275283

276284
useEffect(() => {
277-
if (providedSize && Math.abs(providedSize - size) > 0.5) {
278-
setSize(clamp(providedSize));
285+
if (
286+
!isDragging &&
287+
visualSize != null &&
288+
(providedSize == null || Math.abs(providedSize - visualSize) > 0.5)
289+
) {
290+
onSizeChange?.(Math.round(visualSize));
279291
}
292+
}, [visualSize]);
293+
294+
useEffect(() => {
295+
setTimeout(notifyChange);
280296
}, [providedSize]);
281297

282298
const mods = useMemo(() => {

0 commit comments

Comments
 (0)