From 2adf82f70b2f57fb926a9e4fbb6dc4bea2d514b4 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Wed, 11 Mar 2026 23:03:19 +1300 Subject: [PATCH] fix: suppress hover state during canvas panning to prevent cascading re-renders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace broken `.react-flow__pane.dragging` DOM query guard with a shared `isPanningRef` set by ReactFlow's onMoveStart/onMoveEnd callbacks. Clear hoveredNodeId when panning starts. Use a derived boolean selector in useVideoAutoplay so hovering node A doesn't re-render video nodes B–E. Co-Authored-By: Claude Opus 4.6 --- src/components/WorkflowCanvas.tsx | 6 ++++++ src/components/nodes/BaseNode.tsx | 5 +++-- src/hooks/useVideoAutoplay.ts | 4 +--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/components/WorkflowCanvas.tsx b/src/components/WorkflowCanvas.tsx index 6d28cd68..0536fcdc 100644 --- a/src/components/WorkflowCanvas.tsx +++ b/src/components/WorkflowCanvas.tsx @@ -264,6 +264,9 @@ const findScrollableAncestor = (target: HTMLElement, deltaX: number, deltaY: num return null; }; +/** Shared ref so child components (BaseNode) can check panning state without re-rendering */ +export const isPanningRef = { current: false }; + export function WorkflowCanvas() { const { nodes, edges, groups, isModalOpen, showQuickstart, navigationTarget, canvasNavigationSettings, dimmedNodeIds } = useWorkflowStore(useShallow((state) => ({ @@ -294,6 +297,7 @@ export function WorkflowCanvas() { const setShortcutsDialogOpen = useWorkflowStore((state) => state.setShortcutsDialogOpen); const regenerateNode = useWorkflowStore((state) => state.regenerateNode); const clearWorkflow = useWorkflowStore((state) => state.clearWorkflow); + const setHoveredNodeId = useWorkflowStore((state) => state.setHoveredNodeId); const openAnnotationModal = useAnnotationStore((state) => state.openModal); const { screenToFlowPosition, getViewport, zoomIn, zoomOut, setViewport, setCenter } = useReactFlow(); const { show: showToast } = useToast(); @@ -1953,6 +1957,8 @@ export function WorkflowCanvas() { onEdgesChange={onEdgesChange} onConnect={handleConnect} onConnectEnd={handleConnectEnd} + onMoveStart={() => { isPanningRef.current = true; setHoveredNodeId(null); }} + onMoveEnd={() => { isPanningRef.current = false; }} onNodeDragStop={handleNodeDragStop} onSelectionChange={handleSelectionChange} nodeTypes={nodeTypes} diff --git a/src/components/nodes/BaseNode.tsx b/src/components/nodes/BaseNode.tsx index fa1a6b7b..d5373219 100644 --- a/src/components/nodes/BaseNode.tsx +++ b/src/components/nodes/BaseNode.tsx @@ -3,6 +3,7 @@ import { ReactNode, useCallback, useRef, useLayoutEffect } from "react"; import { Node, NodeResizer, OnResize, useReactFlow } from "@xyflow/react"; import { useWorkflowStore } from "@/store/workflowStore"; +import { isPanningRef } from "@/components/WorkflowCanvas"; import { getMediaDimensions, calculateAspectFitSize } from "@/utils/nodeDimensions"; const DEFAULT_NODE_DIMENSION = 300; @@ -296,11 +297,11 @@ export function BaseNode({ ${className} `} onMouseEnter={() => { - if (document.querySelector('.react-flow__pane.dragging')) return; + if (isPanningRef.current) return; setHoveredNodeId(id); }} onMouseLeave={() => { - if (document.querySelector('.react-flow__pane.dragging')) return; + if (isPanningRef.current) return; setHoveredNodeId(null); }} > diff --git a/src/hooks/useVideoAutoplay.ts b/src/hooks/useVideoAutoplay.ts index dfcc5c2e..761fd6cf 100644 --- a/src/hooks/useVideoAutoplay.ts +++ b/src/hooks/useVideoAutoplay.ts @@ -22,9 +22,7 @@ export function useVideoAutoplay( ): React.RefObject { const videoRef = useRef(null); const hoverTimeoutRef = useRef | null>(null); - const hoveredNodeId = useWorkflowStore((s) => s.hoveredNodeId); - - const isHovered = hoveredNodeId === nodeId; + const isHovered = useWorkflowStore((s) => s.hoveredNodeId === nodeId); useEffect(() => { const video = videoRef.current;