Merge pull request #4725 from xyflow/feat/use-handle-connection

add `useNodeConnections`, deprecate `useHandleConnections`
This commit is contained in:
Moritz Klack
2025-01-07 15:18:11 +01:00
committed by GitHub
21 changed files with 251 additions and 36 deletions

View File

@@ -0,0 +1,7 @@
---
'@xyflow/react': minor
'@xyflow/svelte': patch
'@xyflow/system': patch
---
Add useNodeConnections hook to track all connections to a node. Can be filtered by handleType and handleId.

View File

@@ -49,7 +49,7 @@ import NodeToolbar from '../examples/NodeToolbar';
import UseConnection from '../examples/UseConnection';
import UseNodesInitialized from '../examples/UseNodesInit';
import UseNodesData from '../examples/UseNodesData';
import UseHandleConnections from '../examples/UseHandleConnections';
import UseNodeConnections from '../examples/UseNodeConnections';
import AddNodeOnEdgeDrop from '../examples/AddNodeOnEdgeDrop';
import DevTools from '../examples/DevTools';
import Redux from '../examples/Redux';
@@ -313,9 +313,9 @@ const routes: IRoute[] = [
component: UseReactFlow,
},
{
name: 'useHandleConnections',
path: 'usehandleconnections',
component: UseHandleConnections,
name: 'useNodeConnections',
path: 'usenodeconnections',
component: UseNodeConnections,
},
{
name: 'useNodesData',

View File

@@ -1,5 +1,5 @@
import { memo, FC, useEffect, useCallback } from 'react';
import { Handle, Position, NodeProps, useHandleConnections, Connection, HandleProps } from '@xyflow/react';
import { Handle, Position, NodeProps, useNodeConnections, Connection, HandleProps } from '@xyflow/react';
function CustomHandle({ nodeId, ...handleProps }: HandleProps & { nodeId: string }) {
const onConnect = useCallback(
@@ -11,9 +11,9 @@ function CustomHandle({ nodeId, ...handleProps }: HandleProps & { nodeId: string
(connections: Connection[]) => console.log('onDisconnect handler, node id:', nodeId, connections),
[nodeId]
);
const connections = useHandleConnections({
const connections = useNodeConnections({
type: handleProps.type,
id: handleProps.id,
handleId: handleProps.id,
onConnect,
onDisconnect,
});

View File

@@ -1,5 +1,5 @@
import { memo, FC, useEffect, useCallback } from 'react';
import { Handle, Position, NodeProps, useHandleConnections, Connection, HandleProps } from '@xyflow/react';
import { Handle, Position, NodeProps, useNodeConnections, Connection, HandleProps } from '@xyflow/react';
function CustomHandle({ nodeId, ...handleProps }: HandleProps & { nodeId: string }) {
const onConnect = useCallback(
@@ -14,9 +14,9 @@ function CustomHandle({ nodeId, ...handleProps }: HandleProps & { nodeId: string
},
[nodeId]
);
const connections = useHandleConnections({
const connections = useNodeConnections({
type: handleProps.type,
id: handleProps.id,
handleId: handleProps.id,
onConnect,
onDisconnect,
});

View File

@@ -1,9 +1,9 @@
import { memo } from 'react';
import { Handle, Position, useHandleConnections, useNodesData } from '@xyflow/react';
import { Handle, Position, useNodeConnections, useNodesData } from '@xyflow/react';
import { isTextNode, type MyNode } from '.';
function ResultNode() {
const connections = useHandleConnections({
const connections = useNodeConnections({
type: 'target',
});
const nodesData = useNodesData<MyNode>(connections.map((connection) => connection.source));

View File

@@ -1,10 +1,10 @@
import { memo, useEffect } from 'react';
import { Position, NodeProps, useReactFlow, Handle, useHandleConnections, useNodesData } from '@xyflow/react';
import { Position, NodeProps, useReactFlow, Handle, useNodeConnections, useNodesData } from '@xyflow/react';
import { isTextNode, type TextNode, type MyNode } from '.';
function UppercaseNode({ id }: NodeProps) {
const { updateNodeData } = useReactFlow();
const connections = useHandleConnections({
const connections = useNodeConnections({
type: 'target',
});
const nodesData = useNodesData<MyNode>(connections[0]?.source);

View File

@@ -2,7 +2,7 @@
import {
Handle,
Position,
useHandleConnections,
useNodeConnections,
useNodesData,
type NodeProps
} from '@xyflow/svelte';
@@ -13,7 +13,7 @@
export let id: $$Props['id'];
$$restProps;
const connections = useHandleConnections({
const connections = useNodeConnections({
nodeId: id,
type: 'target'
});

View File

@@ -2,7 +2,7 @@
import {
Handle,
Position,
useHandleConnections,
useNodeConnections,
useNodesData,
useSvelteFlow,
type NodeProps
@@ -16,7 +16,7 @@
$$restProps;
const { updateNodeData } = useSvelteFlow();
const connections = useHandleConnections({
const connections = useNodeConnections({
nodeId: id,
type: 'target'
});

View File

@@ -22,6 +22,7 @@ type useHandleConnectionsParams = {
* Hook to check if a <Handle /> is connected to another <Handle /> and get the connections.
*
* @public
* @deprecated Use `useNodeConnections` instead.
* @param param.type - handle type 'source' or 'target'
* @param param.nodeId - node id - if not provided, the node id from the NodeIdContext is used
* @param param.id - the handle id (this is only needed if the node has multiple handles of the same type)
@@ -31,18 +32,22 @@ type useHandleConnectionsParams = {
*/
export function useHandleConnections({
type,
id = null,
id,
nodeId,
onConnect,
onDisconnect,
}: useHandleConnectionsParams): HandleConnection[] {
console.warn(
'[DEPRECATED] `useHandleConnections` is deprecated. Instead use `useNodeConnections` https://reactflow.dev/api-reference/hooks/useNodeConnections'
);
const _nodeId = useNodeId();
const currentNodeId = nodeId ?? _nodeId;
const prevConnections = useRef<Map<string, HandleConnection> | null>(null);
const connections = useStore(
(state) => state.connectionLookup.get(`${currentNodeId}-${type}-${id}`),
(state) => state.connectionLookup.get(`${currentNodeId}-${type}${id ? `-${id}` : ''}`),
areConnectionMapsEqual
);

View File

@@ -0,0 +1,69 @@
import { useEffect, useMemo, useRef } from 'react';
import {
Connection,
NodeConnection,
HandleType,
areConnectionMapsEqual,
handleConnectionChange,
errorMessages,
} from '@xyflow/system';
import { useStore } from './useStore';
import { useNodeId } from '../contexts/NodeIdContext';
const error014 = errorMessages['error014']();
type UseNodeConnectionsParams = {
type?: HandleType;
handleId?: string;
nodeId?: string;
onConnect?: (connections: Connection[]) => void;
onDisconnect?: (connections: Connection[]) => void;
};
/**
* Hook to retrieve all edges connected to a node. Can be filtered by handle type and id.
*
* @public
* @param param.nodeId - node id - optional if called inside a custom node
* @param param.type - filter by handle type 'source' or 'target'
* @param param.handleId - filter by handle id (this is only needed if the node has multiple handles of the same type)
* @param param.onConnect - gets called when a connection is established
* @param param.onDisconnect - gets called when a connection is removed
* @returns an array with connections
*/
export function useNodeConnections({
type,
handleId,
nodeId,
onConnect,
onDisconnect,
}: UseNodeConnectionsParams = {}): NodeConnection[] {
const _nodeId = useNodeId();
const currentNodeId = nodeId ?? _nodeId;
if (!currentNodeId) {
throw new Error(error014);
}
const prevConnections = useRef<Map<string, NodeConnection> | null>(null);
const connections = useStore(
(state) =>
state.connectionLookup.get(`${currentNodeId}${type ? (handleId ? `-${type}-${handleId}` : `-${type}`) : ''}`),
areConnectionMapsEqual
);
useEffect(() => {
// @todo dicuss if onConnect/onDisconnect should be called when the component mounts/unmounts
if (prevConnections.current && prevConnections.current !== connections) {
const _connections = connections ?? new Map();
handleConnectionChange(prevConnections.current, _connections, onDisconnect);
handleConnectionChange(_connections, prevConnections.current, onConnect);
}
prevConnections.current = connections ?? new Map();
}, [connections, onConnect, onDisconnect]);
return useMemo(() => Array.from(connections?.values() ?? []), [connections]);
}

View File

@@ -238,7 +238,14 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
Array.from(
store
.getState()
.connectionLookup.get(`${nodeId}-${type}-${id ?? null}`)
.connectionLookup.get(`${nodeId}-${type}${id ? `-${id}` : ''}`)
?.values() ?? []
),
getNodeConnections: ({ type, handleId, nodeId }) =>
Array.from(
store
.getState()
.connectionLookup.get(`${nodeId}${type ? (handleId ? `-${type}-${handleId}` : `-${type}`) : ''}`)
?.values() ?? []
),
};

View File

@@ -24,6 +24,7 @@ export { useOnViewportChange, type UseOnViewportChangeOptions } from './hooks/us
export { useOnSelectionChange, type UseOnSelectionChangeOptions } from './hooks/useOnSelectionChange';
export { useNodesInitialized, type UseNodesInitializedOptions } from './hooks/useNodesInitialized';
export { useHandleConnections } from './hooks/useHandleConnections';
export { useNodeConnections } from './hooks/useNodeConnections';
export { useNodesData } from './hooks/useNodesData';
export { useConnection } from './hooks/useConnection';
export { useInternalNode } from './hooks/useInternalNode';

View File

@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-namespace */
import type { HandleConnection, HandleType, Rect, Viewport } from '@xyflow/system';
import type { HandleConnection, HandleType, NodeConnection, Rect, Viewport } from '@xyflow/system';
import type { Node, Edge, ViewportHelperFunctions, InternalNode } from '.';
export type ReactFlowJsonObject<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
@@ -183,7 +183,7 @@ export type GeneralHelpers<NodeType extends Node = Node, EdgeType extends Edge =
getNodesBounds: (nodes: (NodeType | InternalNode | string)[]) => Rect;
/**
* Gets all connections for a given handle belonging to a specific node.
*
* @deprecated
* @param type - handle type 'source' or 'target'
* @param id - the handle id (this is only needed if you have multiple handles of the same type, meaning you have to provide a unique id for each handle)
* @param nodeId - the node id the handle belongs to
@@ -198,6 +198,23 @@ export type GeneralHelpers<NodeType extends Node = Node, EdgeType extends Edge =
nodeId: string;
id?: string | null;
}) => HandleConnection[];
/**
* Gets all connections to a node. Can be filtered by handle type and id.
* @deprecated use `getNodeConnections` instead
* @param type - handle type 'source' or 'target'
* @param handleId - the handle id (this is only needed if you have multiple handles of the same type, meaning you have to provide a unique id for each handle)
* @param nodeId - the node id the handle belongs to
* @returns an array with handle connections
*/
getNodeConnections: ({
type,
handleId,
nodeId,
}: {
type?: HandleType;
nodeId: string;
handleId?: string | null;
}) => NodeConnection[];
};
export type ReactFlowInstance<NodeType extends Node = Node, EdgeType extends Edge = Edge> = GeneralHelpers<

View File

@@ -116,7 +116,7 @@
$: if (onconnect || ondisconnect) {
// connectionLookup is not reactive, so we use edges to get notified about updates
$edges;
connections = $connectionLookup.get(`${nodeId}-${type}-${id || null}`);
connections = $connectionLookup.get(`${nodeId}-${type}${id ? `-${id}` : ''}`);
}
$: {

View File

@@ -16,14 +16,19 @@ const initialConnections: HandleConnection[] = [];
* Hook to check if a <Handle /> is connected to another <Handle /> and get the connections.
*
* @public
* @deprecated Use `useNodeConnections` instead.
* @param param.nodeId
* @param param.type - handle type 'source' or 'target'
* @param param.id - the handle id (this is only needed if the node has multiple handles of the same type)
* @returns an array with connections
*/
export function useHandleConnections({ type, nodeId, id = null }: useHandleConnectionsParams) {
export function useHandleConnections({ type, nodeId, id }: useHandleConnectionsParams) {
const { edges, connectionLookup } = useStore();
console.warn(
'[DEPRECATED] `useHandleConnections` is deprecated. Instead use `useNodeConnections` https://svelteflow.dev/api-reference/hooks/useNodeConnections'
);
const _nodeId = getContext<string>('svelteflow__node_id');
const currentNodeId = nodeId ?? _nodeId;
@@ -32,7 +37,7 @@ export function useHandleConnections({ type, nodeId, id = null }: useHandleConne
return derived(
[edges, connectionLookup],
([, connectionLookup], set) => {
const nextConnections = connectionLookup.get(`${currentNodeId}-${type}-${id || null}`);
const nextConnections = connectionLookup.get(`${currentNodeId}-${type}${id ? `-${id}` : ''}`);
if (!areConnectionMapsEqual(nextConnections, prevConnections)) {
prevConnections = nextConnections;

View File

@@ -0,0 +1,62 @@
import { derived } from 'svelte/store';
import {
areConnectionMapsEqual,
errorMessages,
type NodeConnection,
type HandleType
} from '@xyflow/system';
import { useStore } from '$lib/store';
import { getContext } from 'svelte';
const error014 = errorMessages['error014']();
type UseNodeConnectionsParams = {
type?: HandleType;
handleId?: string;
nodeId?: string;
// TODO: Svelte 5
// onConnect?: (connections: Connection[]) => void;
// onDisconnect?: (connections: Connection[]) => void;
};
const initialConnections: NodeConnection[] = [];
/**
* Hook to retrieve all edges connected to a node. Can be filtered by handle type and id.
*
* @public
* @param param.nodeId - node id - optional if called inside a custom node
* @param param.type - filter by handle type 'source' or 'target'
* @param param.handleId - filter by handle id (this is only needed if the node has multiple handles of the same type)
* @todo @param param.onConnect - gets called when a connection is established
* @todo @param param.onDisconnect - gets called when a connection is removed
* @returns an array with connections
*/
export function useNodeConnections({ type, nodeId, handleId }: UseNodeConnectionsParams = {}) {
const { edges, connectionLookup } = useStore();
const _nodeId = getContext<string>('svelteflow__node_id');
const currentNodeId = nodeId ?? _nodeId;
if (!currentNodeId) {
throw new Error(error014);
}
let prevConnections: Map<string, NodeConnection> | undefined = undefined;
return derived(
[edges, connectionLookup],
([, connectionLookup], set) => {
const nextConnections = connectionLookup.get(
`${currentNodeId}${type ? (handleId ? `-${type}-${handleId}` : `-${type}`) : ''}`
);
if (!areConnectionMapsEqual(nextConnections, prevConnections)) {
prevConnections = nextConnections;
set(Array.from(prevConnections?.values() || []));
}
},
initialConnections
);
}

View File

@@ -31,6 +31,7 @@ export * from '$lib/hooks/useUpdateNodeInternals';
export * from '$lib/hooks/useConnection';
export * from '$lib/hooks/useNodesEdges';
export * from '$lib/hooks/useHandleConnections';
export * from '$lib/hooks/useNodeConnections';
export * from '$lib/hooks/useNodesData';
export * from '$lib/hooks/useInternalNode';
export { useInitialized, useNodesInitialized } from '$lib/hooks/useInitialized';

View File

@@ -24,6 +24,8 @@ export const errorMessages = {
`Node with id "${id}" does not exist, it may have been removed. This can happen when a node is deleted before the "onNodeClick" handler is called.`,
error013: (lib: string = 'react') =>
`It seems that you haven't loaded the styles. Please import '@xyflow/${lib}/dist/style.css' or base.css to make sure everything is working properly.`,
error014: () =>
'useNodeConnections: No node ID found. Call useNodeConnections inside a custom Node or provide a node ID.',
};
export const infiniteExtent: CoordinateExtent = [

View File

@@ -33,10 +33,15 @@ export type Connection = {
targetHandle: string | null;
};
// TODO: remove in next version
export type HandleConnection = Connection & {
edgeId: string;
};
export type NodeConnection = Connection & {
edgeId: string;
};
export enum ConnectionMode {
Strict = 'strict',
Loose = 'loose',

View File

@@ -1,4 +1,4 @@
import { infiniteExtent } from '..';
import { HandleConnection, infiniteExtent } from '..';
import {
NodeBase,
CoordinateExtent,
@@ -42,7 +42,7 @@ const adoptUserNodesDefaultOptions = {
checkEquality: true,
};
function mergeObjects<T extends Record<string, any>>(base: T, incoming?: Partial<T>): T {
function mergeObjects<T extends Record<string, unknown>>(base: T, incoming?: Partial<T>): T {
const result = { ...base };
for (const key in incoming) {
if (incoming[key] !== undefined) {
@@ -439,22 +439,56 @@ export async function panBy({
return Promise.resolve(transformChanged);
}
/**
* this function adds the connection to the connectionLookup
* at the following keys: nodeId-type-handleId, nodeId-type and nodeId
* @param type type of the connection
* @param connection connection that should be added to the lookup
* @param connectionKey at which key the connection should be added
* @param connectionLookup reference to the connection lookup
* @param nodeId nodeId of the connection
* @param handleId handleId of the conneciton
*/
function addConnectionToLookup(
type: 'source' | 'target',
connection: HandleConnection,
connectionKey: string,
connectionLookup: ConnectionLookup,
nodeId: string,
handleId: string | null
) {
// We add the connection to the connectionLookup at the following keys
// 1. nodeId, 2. nodeId-type, 3. nodeId-type-handleId
// If the key already exists, we add the connection to the existing map
let key = nodeId;
const nodeMap = connectionLookup.get(key) || new Map();
connectionLookup.set(key, nodeMap.set(connectionKey, connection));
key = `${nodeId}-${type}`;
const typeMap = connectionLookup.get(key) || new Map();
connectionLookup.set(key, typeMap.set(connectionKey, connection));
if (handleId) {
key = `${nodeId}-${type}-${handleId}`;
const handleMap = connectionLookup.get(key) || new Map();
connectionLookup.set(key, handleMap.set(connectionKey, connection));
}
}
export function updateConnectionLookup(connectionLookup: ConnectionLookup, edgeLookup: EdgeLookup, edges: EdgeBase[]) {
connectionLookup.clear();
edgeLookup.clear();
for (const edge of edges) {
const { source, target, sourceHandle = null, targetHandle = null } = edge;
const { source: sourceNode, target: targetNode, sourceHandle = null, targetHandle = null } = edge;
const sourceKey = `${source}-source-${sourceHandle}`;
const targetKey = `${target}-target-${targetHandle}`;
const connection = { edgeId: edge.id, source: sourceNode, target: targetNode, sourceHandle, targetHandle };
const sourceKey = `${sourceNode}-${sourceHandle}`;
const targetKey = `${targetNode}-${targetHandle}`;
const prevSource = connectionLookup.get(sourceKey) || new Map();
const prevTarget = connectionLookup.get(targetKey) || new Map();
const connection = { edgeId: edge.id, source, target, sourceHandle, targetHandle };
addConnectionToLookup('source', connection, targetKey, connectionLookup, sourceNode, sourceHandle);
addConnectionToLookup('target', connection, sourceKey, connectionLookup, targetNode, targetHandle);
edgeLookup.set(edge.id, edge);
connectionLookup.set(sourceKey, prevSource.set(`${target}-${targetHandle}`, connection));
connectionLookup.set(targetKey, prevTarget.set(`${source}-${sourceHandle}`, connection));
}
}