From 8164576d6c90f2e4e211f868a63fcc76a0bb564d Mon Sep 17 00:00:00 2001 From: lucaronin Date: Tue, 24 Feb 2026 10:13:43 +0100 Subject: [PATCH] fix: use refs in useTabDrag to prevent stale closures on drop handleDrop and handleDragOver closed over dragIndex/dropIndex state values. When the last dragover and drop events fire in the same React render cycle, the drop handler reads stale state (often null), causing computeDropTarget to return null and skip the reorder. Mirror dragIndex/dropIndex into refs that are updated synchronously alongside setState. Event handlers now read from refs, ensuring they always see the latest values regardless of React's batching schedule. Co-Authored-By: Claude Opus 4.6 --- src/components/TabBar.tsx | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/src/components/TabBar.tsx b/src/components/TabBar.tsx index 73ef729a..f0a30826 100644 --- a/src/components/TabBar.tsx +++ b/src/components/TabBar.tsx @@ -100,16 +100,25 @@ function computeInsertIndex(e: React.DragEvent, index: number): function useTabDrag(onReorderTabs?: (from: number, to: number) => void) { const [dragIndex, setDragIndex] = useState(null) const [dropIndex, setDropIndex] = useState(null) + // Refs mirror state so event handlers always read the latest values, + // avoiding stale closures when dragover and drop fire in the same frame. + const dragIndexRef = useRef(null) + const dropIndexRef = useRef(null) const dragNodeRef = useRef(null) + const onReorderRef = useRef(onReorderTabs) + useEffect(() => { onReorderRef.current = onReorderTabs }) const resetDrag = useCallback(() => { if (dragNodeRef.current) dragNodeRef.current.style.opacity = '' dragNodeRef.current = null + dragIndexRef.current = null + dropIndexRef.current = null setDragIndex(null) setDropIndex(null) }, []) const handleDragStart = useCallback((e: React.DragEvent, index: number) => { + dragIndexRef.current = index setDragIndex(index) e.dataTransfer.effectAllowed = 'move' e.dataTransfer.setData('text/plain', String(index)) @@ -122,20 +131,32 @@ function useTabDrag(onReorderTabs?: (from: number, to: number) => void) { const handleDragOver = useCallback((e: React.DragEvent, index: number) => { e.preventDefault() e.dataTransfer.dropEffect = 'move' - if (dragIndex === null || dragIndex === index) { setDropIndex(null); return } - setDropIndex(computeInsertIndex(e, index)) - }, [dragIndex]) + const currentDrag = dragIndexRef.current + if (currentDrag === null || currentDrag === index) { + dropIndexRef.current = null + setDropIndex(null) + return + } + const idx = computeInsertIndex(e, index) + dropIndexRef.current = idx + setDropIndex(idx) + }, []) const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault() - const toIndex = computeDropTarget(dragIndex, dropIndex) - if (toIndex !== null && onReorderTabs) onReorderTabs(dragIndex!, toIndex) + const toIndex = computeDropTarget(dragIndexRef.current, dropIndexRef.current) + if (toIndex !== null && onReorderRef.current) { + onReorderRef.current(dragIndexRef.current!, toIndex) + } resetDrag() - }, [dragIndex, dropIndex, onReorderTabs, resetDrag]) + }, [resetDrag]) const handleBarDragLeave = useCallback((e: React.DragEvent) => { const related = e.relatedTarget as HTMLElement | null - if (!e.currentTarget.contains(related)) setDropIndex(null) + if (!e.currentTarget.contains(related)) { + dropIndexRef.current = null + setDropIndex(null) + } }, []) return { dragIndex, dropIndex, handleDragStart, handleDragEnd: resetDrag, handleDragOver, handleDrop, handleBarDragLeave }