From 3d7bc6241c7625cc33787036b4a82806283e062b Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 9 Jun 2026 09:34:44 +0300 Subject: [PATCH] fix: avoid flicker when controlled pane sizes also change content --- src/components/SplitPane.tsx | 6 ++++-- src/hooks/useResizer.ts | 5 ++++- src/utils/useIsomorphicLayoutEffect.ts | 7 +++++++ 3 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 src/utils/useIsomorphicLayoutEffect.ts diff --git a/src/components/SplitPane.tsx b/src/components/SplitPane.tsx index 6fc8f277..701c0778 100644 --- a/src/components/SplitPane.tsx +++ b/src/components/SplitPane.tsx @@ -14,6 +14,7 @@ import { useResizer } from '../hooks/useResizer'; import { useKeyboardResize } from '../hooks/useKeyboardResize'; import { convertToPixels, distributeSizes } from '../utils/calculations'; import { cn } from '../utils/classNames'; +import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect'; const DEFAULT_CLASSNAME = 'split-pane'; const MIN_PANES = 2; @@ -158,8 +159,9 @@ export function SplitPane(props: SplitPaneProps) { ); // Sync paneSizes with controlled size props when they change - // This handles the case where parent state is reset (e.g., clicking a "Reset" button) - useEffect(() => { + // This handles the case where parent state is reset (e.g., clicking a "Reset" button). + // Runs in a layout effect so the new width is committed in the same paint as possible content changes. + useIsomorphicLayoutEffect(() => { if (containerSize === 0) return; // Check if any pane has a controlled size prop diff --git a/src/hooks/useResizer.ts b/src/hooks/useResizer.ts index 348fc613..6b3a9893 100644 --- a/src/hooks/useResizer.ts +++ b/src/hooks/useResizer.ts @@ -5,6 +5,7 @@ import { snapToPoint, applyStep, } from '../utils/calculations'; +import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect'; /** * Options for the useResizer hook. @@ -85,8 +86,10 @@ export function useResizer(options: UseResizerOptions): UseResizerResult { onResizeEndRef.current = onResizeEnd; // Sync sizes from props when not dragging (React 19 compatible) + // Layout effect so an external size change (e.g. a controlled pane collapsing) + // updates the rendered width in lockstep with consumer content. const sizesRef = useRef(sizes); - useEffect(() => { + useIsomorphicLayoutEffect(() => { if ( !isDragging && JSON.stringify(sizes) !== JSON.stringify(sizesRef.current) diff --git a/src/utils/useIsomorphicLayoutEffect.ts b/src/utils/useIsomorphicLayoutEffect.ts new file mode 100644 index 00000000..4e79d557 --- /dev/null +++ b/src/utils/useIsomorphicLayoutEffect.ts @@ -0,0 +1,7 @@ +import { useEffect, useLayoutEffect } from 'react'; + +/** + * `useLayoutEffect` that degrades to `useEffect` during server rendering. + */ +export const useIsomorphicLayoutEffect = + typeof window === 'undefined' ? useEffect : useLayoutEffect;