From 03b8e1ef195fd98cfefd337f1e4a6a37b92a3f5a Mon Sep 17 00:00:00 2001 From: ysds Date: Sat, 24 Jan 2026 02:29:02 +0900 Subject: [PATCH 1/7] fix: ensure drag handlers update when nodes selection changes programmatically Fixes #4841 When nodes were programmatically selected (e.g., adding a new node with `selected: true`), the nodes selection rectangle would appear but dragging it would not work. Changes: - Consolidated duplicate effect hooks in useDrag into a single useIsomorphicLayoutEffect - Added `disabled` parameter to useDrag to allow external control - NodesSelection now passes `disabled: !shouldRender` to useDrag - Improved condition clarity with `shouldRender` variable This ensures drag handlers are properly set up when nodeRef.current becomes available, even when the selection state changes after the initial render. --- .../src/components/NodesSelection/index.tsx | 5 ++- packages/react/src/hooks/useDrag.ts | 35 +++++++++++-------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/packages/react/src/components/NodesSelection/index.tsx b/packages/react/src/components/NodesSelection/index.tsx index ecaef17b0e..de118a4495 100644 --- a/packages/react/src/components/NodesSelection/index.tsx +++ b/packages/react/src/components/NodesSelection/index.tsx @@ -51,11 +51,14 @@ export function NodesSelection({ } }, [disableKeyboardA11y]); + const shouldRender = !userSelectionActive && width !== null && height !== null; + useDrag({ nodeRef, + disabled: !shouldRender, }); - if (userSelectionActive || !width || !height) { + if (!shouldRender) { return null; } diff --git a/packages/react/src/hooks/useDrag.ts b/packages/react/src/hooks/useDrag.ts index f7943f9871..f33c1276ea 100644 --- a/packages/react/src/hooks/useDrag.ts +++ b/packages/react/src/hooks/useDrag.ts @@ -3,6 +3,7 @@ import { XYDrag, type XYDragInstance } from '@xyflow/system'; import { handleNodeClick } from '../components/Nodes/utils'; import { useStoreApi } from './useStore'; +import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'; type UseDragParams = { nodeRef: RefObject; @@ -51,23 +52,29 @@ export function useDrag({ }); }, []); - useEffect(() => { + useIsomorphicLayoutEffect(() => { if (disabled) { xyDrag.current?.destroy(); - } else if (nodeRef.current) { - xyDrag.current?.update({ - noDragClassName, - handleSelector, - domNode: nodeRef.current, - isSelectable, - nodeId, - nodeClickDistance, - }); - return () => { - xyDrag.current?.destroy(); - }; + return; + } + + if (!nodeRef.current || !xyDrag.current) { + return; } - }, [noDragClassName, handleSelector, disabled, isSelectable, nodeRef, nodeId]); + + xyDrag.current.update({ + noDragClassName, + handleSelector, + domNode: nodeRef.current, + isSelectable, + nodeId, + nodeClickDistance, + }); + + return () => { + xyDrag.current?.destroy(); + }; + }, [noDragClassName, handleSelector, disabled, isSelectable, nodeRef, nodeId, nodeClickDistance]); return dragging; } From 382c654c315accca2005e39d477eed6649f12e40 Mon Sep 17 00:00:00 2001 From: ysds Date: Sat, 24 Jan 2026 03:01:29 +0900 Subject: [PATCH 2/7] chore(changeset): add changeset for drag handler fix --- .changeset/tender-ways-attend.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tender-ways-attend.md diff --git a/.changeset/tender-ways-attend.md b/.changeset/tender-ways-attend.md new file mode 100644 index 0000000000..5973788497 --- /dev/null +++ b/.changeset/tender-ways-attend.md @@ -0,0 +1,5 @@ +--- +'@xyflow/react': patch +--- + +Consolidate drag handler effects in useDrag to fix programmatic selection issues From ee53f8baf7090a8e000eb28e97a6fd7bcceb7087 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Fri, 6 Feb 2026 19:41:24 +0100 Subject: [PATCH 3/7] add reproduction of the bug --- examples/react/src/App/routes.ts | 6 +++ .../src/examples/NodeSelectionBug/index.tsx | 41 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 examples/react/src/examples/NodeSelectionBug/index.tsx diff --git a/examples/react/src/App/routes.ts b/examples/react/src/App/routes.ts index 055dc9e149..d5bbfe0150 100644 --- a/examples/react/src/App/routes.ts +++ b/examples/react/src/App/routes.ts @@ -62,6 +62,7 @@ import MovingHandles from '../examples/MovingHandles'; import DetachedHandle from '../examples/DetachedHandle'; import ZIndexMode from '../examples/ZIndexMode'; import Middlewares from '../examples/Middlewares'; +import NodeSelectionBug from '../examples/NodeSelectionBug'; export interface IRoute { name: string; @@ -390,6 +391,11 @@ const routes: IRoute[] = [ path: 'z-index-mode', component: ZIndexMode, }, + { + name: 'Node Selection Bug', + path: 'node-selection-bug', + component: NodeSelectionBug, + }, ]; export default routes; diff --git a/examples/react/src/examples/NodeSelectionBug/index.tsx b/examples/react/src/examples/NodeSelectionBug/index.tsx new file mode 100644 index 0000000000..a587cab392 --- /dev/null +++ b/examples/react/src/examples/NodeSelectionBug/index.tsx @@ -0,0 +1,41 @@ +import { Node, ReactFlow, useNodesState } from '@xyflow/react'; + +import '@xyflow/react/dist/style.css'; +import { useRef } from 'react'; + +export default function App() { + const [nodes, setNodes, onNodesChange] = useNodesState([ + { + id: '0', + position: { x: 0, y: 0 }, + data: { label: 'Rectangle Select Me First' }, + }, + ]); + const id = useRef(0); + return ( + <> + + + + ); +} From cb41f444eb51354c477681447c363084d507e953 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Mon, 9 Feb 2026 07:57:41 +0100 Subject: [PATCH 4/7] simplify code & useEffect instead of uselayoutEffect --- packages/react/src/hooks/useDrag.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/react/src/hooks/useDrag.ts b/packages/react/src/hooks/useDrag.ts index f33c1276ea..6b4c7642bd 100644 --- a/packages/react/src/hooks/useDrag.ts +++ b/packages/react/src/hooks/useDrag.ts @@ -3,7 +3,6 @@ import { XYDrag, type XYDragInstance } from '@xyflow/system'; import { handleNodeClick } from '../components/Nodes/utils'; import { useStoreApi } from './useStore'; -import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'; type UseDragParams = { nodeRef: RefObject; @@ -52,13 +51,8 @@ export function useDrag({ }); }, []); - useIsomorphicLayoutEffect(() => { - if (disabled) { - xyDrag.current?.destroy(); - return; - } - - if (!nodeRef.current || !xyDrag.current) { + useEffect(() => { + if (disabled || !nodeRef.current || !xyDrag.current) { return; } From 45a4a977c584d7d9f14030137e77aebebaf209fc Mon Sep 17 00:00:00 2001 From: peterkogo Date: Tue, 10 Feb 2026 16:02:43 +0100 Subject: [PATCH 5/7] Always use latest version of onConnectEnd & isValidConnection in Handle/ReconnectAnchors --- .../components/EdgeWrapper/EdgeUpdateAnchors.tsx | 6 ++---- packages/react/src/components/Handle/index.tsx | 4 ++-- .../EdgeReconnectAnchor.svelte | 4 ++-- .../src/lib/components/Handle/Handle.svelte | 15 ++++----------- 4 files changed, 10 insertions(+), 19 deletions(-) diff --git a/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx b/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx index 36a983aa09..87504702cb 100644 --- a/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx +++ b/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx @@ -53,12 +53,10 @@ export function EdgeUpdateAnchors({ const { autoPanOnConnect, domNode, - isValidConnection, connectionMode, connectionRadius, lib, onConnectStart, - onConnectEnd, cancelConnection, nodeLookup, rfId: flowId, @@ -93,10 +91,10 @@ export function EdgeUpdateAnchors({ flowId, cancelConnection, panBy, - isValidConnection, + isValidConnection: (...args) => store.getState().isValidConnection?.(...args) ?? true, onConnect: onConnectEdge, onConnectStart: _onConnectStart, - onConnectEnd, + onConnectEnd: (...args) => store.getState().onConnectEnd?.(...args), onReconnectEnd: _onReconnectEnd, updateConnection, getTransform: () => store.getState().transform, diff --git a/packages/react/src/components/Handle/index.tsx b/packages/react/src/components/Handle/index.tsx index dec2d242e7..1f0dc83413 100644 --- a/packages/react/src/components/Handle/index.tsx +++ b/packages/react/src/components/Handle/index.tsx @@ -142,10 +142,10 @@ function HandleComponent( panBy: currentStore.panBy, cancelConnection: currentStore.cancelConnection, onConnectStart: currentStore.onConnectStart, - onConnectEnd: currentStore.onConnectEnd, + onConnectEnd: (...args) => store.getState().onConnectEnd?.(...args), updateConnection: currentStore.updateConnection, onConnect: onConnectExtended, - isValidConnection: isValidConnection || currentStore.isValidConnection, + isValidConnection: isValidConnection || ((...args) => store.getState().isValidConnection?.(...args) ?? true), getTransform: () => store.getState().transform, getFromHandle: () => store.getState().connection.fromHandle, autoPanSpeed: currentStore.autoPanSpeed, diff --git a/packages/svelte/src/lib/components/EdgeReconnectAnchor/EdgeReconnectAnchor.svelte b/packages/svelte/src/lib/components/EdgeReconnectAnchor/EdgeReconnectAnchor.svelte index 2a4975df3b..dbca1acf17 100644 --- a/packages/svelte/src/lib/components/EdgeReconnectAnchor/EdgeReconnectAnchor.svelte +++ b/packages/svelte/src/lib/components/EdgeReconnectAnchor/EdgeReconnectAnchor.svelte @@ -79,9 +79,9 @@ flowId, cancelConnection, panBy, - isValidConnection, + isValidConnection: (...args) => store.isValidConnection?.(...args) ?? true, onConnectStart: _onConnectStart, - onConnectEnd: onconnectend, + onConnectEnd: (...args) => store.onconnectend?.(...args), onConnect: (connection) => { const reconnectedEdge = { ...edge, ...connection }; const newEdge = onbeforereconnect diff --git a/packages/svelte/src/lib/components/Handle/Handle.svelte b/packages/svelte/src/lib/components/Handle/Handle.svelte index 15f811b1a5..e77562891d 100644 --- a/packages/svelte/src/lib/components/Handle/Handle.svelte +++ b/packages/svelte/src/lib/components/Handle/Handle.svelte @@ -125,21 +125,14 @@ autoPanOnConnect: store.autoPanOnConnect, autoPanSpeed: store.autoPanSpeed, flowId: store.flowId, - isValidConnection: isValidConnection ?? store.isValidConnection, + isValidConnection: + isValidConnection || ((...args) => store.isValidConnection?.(...args) ?? true), updateConnection: store.updateConnection, cancelConnection: store.cancelConnection, panBy: store.panBy, onConnect: onConnectExtended, - onConnectStart: (event, startParams) => { - store.onconnectstart?.(event, { - nodeId: startParams.nodeId, - handleId: startParams.handleId, - handleType: startParams.handleType - }); - }, - onConnectEnd: (event, connectionState) => { - store.onconnectend?.(event, connectionState); - }, + onConnectStart: store.onconnectstart, + onConnectEnd: (...args) => store.onconnectend?.(...args), getTransform: () => [store.viewport.x, store.viewport.y, store.viewport.zoom], getFromHandle: () => store.connection.fromHandle, dragThreshold: store.connectionDragThreshold, From c91d3d022f4517f4403a898cd02ee891b7e1f2d2 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Wed, 18 Feb 2026 10:49:43 +0100 Subject: [PATCH 6/7] chore(changeset) --- .changeset/blue-shirts-beg.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/blue-shirts-beg.md diff --git a/.changeset/blue-shirts-beg.md b/.changeset/blue-shirts-beg.md new file mode 100644 index 0000000000..dcbf1a5443 --- /dev/null +++ b/.changeset/blue-shirts-beg.md @@ -0,0 +1,6 @@ +--- +'@xyflow/react': patch +'@xyflow/svelte': patch +--- + +Fix updating onConnectEnd and isValidConnection after connection started From e3c1f42e9a32b07f1f1ef65ee9685a1c34fa017d Mon Sep 17 00:00:00 2001 From: Moritz Klack Date: Wed, 18 Feb 2026 11:15:02 +0100 Subject: [PATCH 7/7] Update connection status during ongoing connection --- .changeset/blue-shirts-beg.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/blue-shirts-beg.md b/.changeset/blue-shirts-beg.md index dcbf1a5443..21713fb514 100644 --- a/.changeset/blue-shirts-beg.md +++ b/.changeset/blue-shirts-beg.md @@ -3,4 +3,4 @@ '@xyflow/svelte': patch --- -Fix updating onConnectEnd and isValidConnection after connection started +Keep `onConnectEnd` and `isValidConnection` up to date in an ongoing connection