diff --git a/.changeset/dirty-tools-switch.md b/.changeset/dirty-tools-switch.md
new file mode 100644
index 00000000..d07b3e1d
--- /dev/null
+++ b/.changeset/dirty-tools-switch.md
@@ -0,0 +1,5 @@
+---
+'@reactflow/core': patch
+---
+
+handles: handles on top of each other, reduce re-renderings
diff --git a/examples/vite-app/src/examples/EasyConnect/CustomNode.tsx b/examples/vite-app/src/examples/EasyConnect/CustomNode.tsx
index 989df5fa..bd107c93 100644
--- a/examples/vite-app/src/examples/EasyConnect/CustomNode.tsx
+++ b/examples/vite-app/src/examples/EasyConnect/CustomNode.tsx
@@ -2,8 +2,9 @@ import { Handle, NodeProps, Position, ReactFlowState, useStore } from 'reactflow
const connectionNodeIdSelector = (state: ReactFlowState) => state.connectionNodeId;
-export default function CustomNode({ id, isConnectable }: NodeProps) {
+export default function CustomNode({ id }: NodeProps) {
const connectionNodeId = useStore(connectionNodeIdSelector);
+ const isConnecting = !!connectionNodeId;
const isTarget = connectionNodeId && connectionNodeId !== id;
const targetHandleStyle = { zIndex: isTarget ? 3 : 1 };
@@ -18,19 +19,16 @@ export default function CustomNode({ id, isConnectable }: NodeProps) {
backgroundColor: isTarget ? '#ffcce3' : '#ccd9f6',
}}
>
+ {!isConnecting && (
+
+ )}
+
-
{label}
diff --git a/examples/vite-app/src/examples/EasyConnect/style.css b/examples/vite-app/src/examples/EasyConnect/style.css
index c2ebc0a2..8e06c4ea 100644
--- a/examples/vite-app/src/examples/EasyConnect/style.css
+++ b/examples/vite-app/src/examples/EasyConnect/style.css
@@ -28,19 +28,7 @@
border: 2px solid #222138;
}
-div.sourceHandle {
- width: 100%;
- height: 100%;
- position: absolute;
- top: 0;
- left: 0;
- border-radius: 0;
- transform: none;
- border: none;
- opacity: 0;
-}
-
-div.targetHandle {
+div.customHandle {
width: 100%;
height: 100%;
background: blue;
diff --git a/packages/core/src/components/Handle/handler.ts b/packages/core/src/components/Handle/handler.ts
index 9825295b..0b598dec 100644
--- a/packages/core/src/components/Handle/handler.ts
+++ b/packages/core/src/components/Handle/handler.ts
@@ -51,7 +51,7 @@ export function handlePointerDown({
cancelConnection,
} = getState();
let autoPanId = 0;
- let prevClosestHandle: ConnectionHandle | null;
+ let closestHandle: ConnectionHandle | null;
const { x, y } = getEventPosition(event);
const clickedHandle = doc?.elementFromPoint(x, y);
@@ -106,9 +106,9 @@ export function handlePointerDown({
function onPointerMove(event: MouseEvent | TouchEvent) {
const { transform } = getState();
- connectionPosition = getEventPosition(event, containerBounds);
- prevClosestHandle = getClosestHandle(
+ connectionPosition = getEventPosition(event, containerBounds);
+ closestHandle = getClosestHandle(
pointToRendererPoint(connectionPosition, transform, false, [1, 1]),
connectionRadius,
handleLookup
@@ -121,7 +121,7 @@ export function handlePointerDown({
const result = isValidHandle(
event,
- prevClosestHandle,
+ closestHandle,
connectionMode,
nodeId,
handleId,
@@ -136,20 +136,20 @@ export function handlePointerDown({
setState({
connectionPosition:
- prevClosestHandle && isValid
+ closestHandle && isValid
? rendererPointToPoint(
{
- x: prevClosestHandle.x,
- y: prevClosestHandle.y,
+ x: closestHandle.x,
+ y: closestHandle.y,
},
transform
)
: connectionPosition,
- connectionStatus: getConnectionStatus(!!prevClosestHandle, isValid),
+ connectionStatus: getConnectionStatus(!!closestHandle, isValid),
connectionEndHandle: result.endHandle,
});
- if (!prevClosestHandle && !isValid && !handleDomNode) {
+ if (!closestHandle && !isValid && !handleDomNode) {
return resetRecentHandle(prevActiveHandle);
}
@@ -164,7 +164,7 @@ export function handlePointerDown({
}
function onPointerUp(event: MouseEvent | TouchEvent) {
- if ((prevClosestHandle || handleDomNode) && connection && isValid) {
+ if ((closestHandle || handleDomNode) && connection && isValid) {
onConnect?.(connection);
}
diff --git a/packages/core/src/components/Handle/index.tsx b/packages/core/src/components/Handle/index.tsx
index dcd95d7c..ca724285 100644
--- a/packages/core/src/components/Handle/index.tsx
+++ b/packages/core/src/components/Handle/index.tsx
@@ -62,7 +62,7 @@ const Handle = forwardRef(
const store = useStoreApi();
const nodeId = useNodeId();
const { connectOnClick, noPanClassName } = useStore(selector, shallow);
- const { connecting, clickConnecting } = useStore(connectingSelector(nodeId, handleId, type));
+ const { connecting, clickConnecting } = useStore(connectingSelector(nodeId, handleId, type), shallow);
if (!nodeId) {
store.getState().onError?.('010', errorMessages['error010']());
diff --git a/packages/core/src/components/Handle/utils.ts b/packages/core/src/components/Handle/utils.ts
index b8e8cf6a..353b249a 100644
--- a/packages/core/src/components/Handle/utils.ts
+++ b/packages/core/src/components/Handle/utils.ts
@@ -41,18 +41,30 @@ export function getClosestHandle(
connectionRadius: number,
handles: ConnectionHandle[]
): ConnectionHandle | null {
- let closestHandle: ConnectionHandle | null = null;
+ let closestHandles: ConnectionHandle[] = [];
let minDistance = Infinity;
handles.forEach((handle) => {
const distance = Math.sqrt(Math.pow(handle.x - pos.x, 2) + Math.pow(handle.y - pos.y, 2));
- if (distance <= connectionRadius && distance < minDistance) {
+ if (distance <= connectionRadius) {
+ if (distance < minDistance) {
+ closestHandles = [handle];
+ } else if (distance === minDistance) {
+ // when multiple handles are on the same distance we collect all of them
+ closestHandles.push(handle);
+ }
minDistance = distance;
- closestHandle = handle;
}
});
- return closestHandle;
+ if (!closestHandles.length) {
+ return null;
+ }
+
+ return closestHandles.length === 1
+ ? closestHandles[0]
+ : // if multiple handles are layouted on top of each other we take the one with type = target because it's more likely that the user wants to connect to this one
+ closestHandles.find((handle) => handle.type === 'target') || closestHandles[0];
}
type Result = {
@@ -81,6 +93,8 @@ export function isValidHandle(
);
const { x, y } = getEventPosition(event);
const handleBelow = doc.elementFromPoint(x, y);
+ // we always want to prioritize the handle below the mouse cursor over the closest distance handle,
+ // because it could be that the center of another handle is closer to the mouse pointer than the handle below the cursor
const handleToCheck = handleBelow?.classList.contains('react-flow__handle') ? handleBelow : handleDomNode;
const result: Result = {