refactor(react): improve performance by simplify edge renderer

This commit is contained in:
moklick
2023-06-08 12:27:37 +02:00
parent 864dc262c2
commit 4707a07c8b
5 changed files with 171 additions and 124 deletions

View File

@@ -1,14 +1,14 @@
import { memo, useState, useMemo, useRef, type ComponentType, type KeyboardEvent } from 'react';
import { memo, useState, useMemo, useRef, type ComponentType, type KeyboardEvent, useEffect } from 'react';
import cc from 'classcat';
import { getMarkerId, elementSelectionKeys, XYHandle, type Connection } from '@xyflow/system';
import { useStoreApi } from '../../hooks/useStore';
import { useStoreApi, useStore } from '../../hooks/useStore';
import { ARIA_EDGE_DESC_KEY } from '../A11yDescriptions';
import { EdgeAnchor } from './EdgeAnchor';
import { getMouseHandler } from './utils';
import type { EdgeProps, WrapEdgeProps } from '../../types';
const alwaysValidConnection = () => true;
import { getEdgePosition } from '../../hooks/useVisibleEdges';
import { shallow } from 'zustand/shallow';
export default (EdgeComponent: ComponentType<EdgeProps>) => {
const EdgeWrapper = ({
@@ -29,12 +29,6 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
style,
source,
target,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
isSelectable,
hidden,
sourceHandleId,
@@ -60,11 +54,34 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
const [updateHover, setUpdateHover] = useState<boolean>(false);
const [updating, setUpdating] = useState<boolean>(false);
const store = useStoreApi();
const edgePosition = useStore((state) => {
const sourceNode = state.nodeInternals.get(source);
const targetNode = state.nodeInternals.get(target);
if (!sourceNode || !targetNode) {
return null;
}
const pos = getEdgePosition({
sourceNode,
targetNode,
sourceHandle: sourceHandleId || null,
targetHandle: targetHandleId || null,
connectionMode: state.connectionMode,
onError: state.onError,
});
return pos;
}, shallow);
useEffect(() => {
// console.log(edgePosition);
}, [edgePosition]);
const markerStartUrl = useMemo(() => `url(#${getMarkerId(markerStart, rfId)})`, [markerStart, rfId]);
const markerEndUrl = useMemo(() => `url(#${getMarkerId(markerEnd, rfId)})`, [markerEnd, rfId]);
if (hidden) {
if (hidden || !edgePosition) {
return null;
}
@@ -211,12 +228,12 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
labelBgBorderRadius={labelBgBorderRadius}
data={data}
style={style}
sourceX={sourceX}
sourceY={sourceY}
targetX={targetX}
targetY={targetY}
sourcePosition={sourcePosition}
targetPosition={targetPosition}
sourceX={edgePosition.sourceX}
sourceY={edgePosition.sourceY}
targetX={edgePosition.targetX}
targetY={edgePosition.targetY}
sourcePosition={edgePosition.sourcePosition}
targetPosition={edgePosition.targetPosition}
sourceHandleId={sourceHandleId}
targetHandleId={targetHandleId}
markerStart={markerStartUrl}
@@ -229,9 +246,9 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
<>
{(isUpdatable === 'source' || isUpdatable === true) && (
<EdgeAnchor
position={sourcePosition}
centerX={sourceX}
centerY={sourceY}
position={edgePosition.sourcePosition}
centerX={edgePosition.sourceX}
centerY={edgePosition.sourceY}
radius={edgeUpdaterRadius}
onMouseDown={onEdgeUpdaterSourceMouseDown}
onMouseEnter={onEdgeUpdaterMouseEnter}
@@ -241,9 +258,9 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
)}
{(isUpdatable === 'target' || isUpdatable === true) && (
<EdgeAnchor
position={targetPosition}
centerX={targetX}
centerY={targetY}
position={edgePosition.targetPosition}
centerX={edgePosition.targetX}
centerY={edgePosition.targetY}
radius={edgeUpdaterRadius}
onMouseDown={onEdgeUpdaterTargetMouseDown}
onMouseEnter={onEdgeUpdaterMouseEnter}

View File

@@ -1,14 +1,13 @@
import { memo, ReactNode } from 'react';
import { shallow } from 'zustand/shallow';
import cc from 'classcat';
import { errorMessages, ConnectionMode, Position, getHandle } from '@xyflow/system';
import { errorMessages } from '@xyflow/system';
import { useStore } from '../../hooks/useStore';
import useVisibleEdges from '../../hooks/useVisibleEdges';
import MarkerDefinitions from './MarkerDefinitions';
import { getEdgePositions, getNodeData } from './utils';
import { GraphViewProps } from '../GraphView';
import type { Edge, ReactFlowState } from '../../types';
import type { ReactFlowState } from '../../types';
type EdgeRendererProps = Pick<
GraphViewProps,
@@ -35,14 +34,12 @@ type EdgeRendererProps = Pick<
};
const selector = (s: ReactFlowState) => ({
nodesConnectable: s.nodesConnectable,
width: s.width,
height: s.height,
edgesFocusable: s.edgesFocusable,
edgesUpdatable: s.edgesUpdatable,
elementsSelectable: s.elementsSelectable,
width: s.width,
height: s.height,
connectionMode: s.connectionMode,
nodeInternals: s.nodeInternals,
onError: s.onError,
});
@@ -65,9 +62,8 @@ const EdgeRenderer = ({
onEdgeUpdateEnd,
children,
}: EdgeRendererProps) => {
const { edgesFocusable, edgesUpdatable, elementsSelectable, width, height, connectionMode, nodeInternals, onError } =
useStore(selector, shallow);
const edgeTree = useVisibleEdges(onlyRenderVisibleElements, nodeInternals, elevateEdgesOnSelect);
const { width, height, edgesFocusable, edgesUpdatable, elementsSelectable, onError } = useStore(selector, shallow);
const edgeTree = useVisibleEdges(onlyRenderVisibleElements, elevateEdgesOnSelect);
if (!width) {
return null;
@@ -85,14 +81,7 @@ const EdgeRenderer = ({
>
{isMaxLevel && <MarkerDefinitions defaultColor={defaultMarkerColor} rfId={rfId} />}
<g>
{edges.map((edge: Edge) => {
const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData(nodeInternals.get(edge.source));
const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(nodeInternals.get(edge.target));
if (!sourceIsValid || !targetIsValid) {
return null;
}
{edges.map((edge) => {
let edgeType = edge.type || 'default';
if (!edgeTypes[edgeType]) {
@@ -100,16 +89,7 @@ const EdgeRenderer = ({
edgeType = 'default';
}
const EdgeComponent = edgeTypes[edgeType] || edgeTypes.default;
// when connection type is loose we can define all handles as sources and connect source -> source
const targetNodeHandles =
connectionMode === ConnectionMode.Strict
? targetHandleBounds!.target
: (targetHandleBounds!.target ?? []).concat(targetHandleBounds!.source ?? []);
const sourceHandle = getHandle(sourceHandleBounds!.source!, edge.sourceHandle);
const targetHandle = getHandle(targetNodeHandles!, edge.targetHandle);
const sourcePosition = sourceHandle?.position || Position.Bottom;
const targetPosition = targetHandle?.position || Position.Top;
const EdgeComponent = edgeTypes[edgeType];
const isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined'));
const isUpdatable =
typeof onEdgeUpdate !== 'undefined' &&
@@ -119,27 +99,12 @@ const EdgeRenderer = ({
(elementsSelectable && typeof edge.selectable === 'undefined')
);
if (!sourceHandle || !targetHandle) {
onError?.('008', errorMessages['error008'](sourceHandle, edge));
return null;
}
const { sourceX, sourceY, targetX, targetY } = getEdgePositions(
sourceNodeRect,
sourceHandle,
sourcePosition,
targetNodeRect,
targetHandle,
targetPosition
);
return (
<EdgeComponent
key={edge.id}
id={edge.id}
className={cc([edge.className, noPanClassName])}
type={edgeType}
type={edge.type}
data={edge.data}
selected={!!edge.selected}
animated={!!edge.animated}
@@ -157,12 +122,6 @@ const EdgeRenderer = ({
targetHandleId={edge.targetHandle}
markerEnd={edge.markerEnd}
markerStart={edge.markerStart}
sourceX={sourceX}
sourceY={sourceY}
targetX={targetX}
targetY={targetY}
sourcePosition={sourcePosition}
targetPosition={targetPosition}
isSelectable={isSelectable}
onEdgeUpdate={onEdgeUpdate}
onContextMenu={onEdgeContextMenu}

View File

@@ -1,13 +1,29 @@
import { useCallback } from 'react';
import { internalsSymbol, isNumeric } from '@xyflow/system';
import {
BaseNode,
internalsSymbol,
isNumeric,
errorMessages,
ConnectionMode,
Position,
getHandle,
OnError,
} from '@xyflow/system';
import { useStore } from '../hooks/useStore';
import { isEdgeVisible } from '../container/EdgeRenderer/utils';
import { type ReactFlowState, type NodeInternals, type Edge } from '../types';
import { getEdgePositions, getNodeData, isEdgeVisible } from '../container/EdgeRenderer/utils';
import { type ReactFlowState, type NodeInternals, type Edge, EdgePosition } from '../types';
import { shallow } from 'zustand/shallow';
const defaultEdgeTree = [{ level: 0, isMaxLevel: true, edges: [] }];
function groupEdgesByZLevel(edges: Edge[], nodeInternals: NodeInternals, elevateEdgesOnSelect = false) {
type GroupedEdges = {
edges: Edge[];
level: number;
isMaxLevel: boolean;
};
function groupEdgesByZLevel(edges: Edge[], nodeInternals: NodeInternals, elevateEdgesOnSelect = false): GroupedEdges[] {
let maxLevel = -1;
const levelLookup = edges.reduce<Record<string, Edge[]>>((tree, edge) => {
@@ -51,42 +67,104 @@ function groupEdgesByZLevel(edges: Edge[], nodeInternals: NodeInternals, elevate
return edgeTree;
}
function useVisibleEdges(onlyRenderVisible: boolean, nodeInternals: NodeInternals, elevateEdgesOnSelect: boolean) {
type LayoutEdgeParams = {
sourceNode: BaseNode;
sourceHandle: string | null;
targetNode: BaseNode;
targetHandle: string | null;
connectionMode: ConnectionMode;
onError?: OnError;
};
export function getEdgePosition(params: LayoutEdgeParams): EdgePosition | null {
const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData(params.sourceNode);
const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(params.targetNode);
if (!sourceIsValid || !targetIsValid) {
return null;
}
// when connection type is loose we can define all handles as sources and connect source -> source
const targetNodeHandles =
params.connectionMode === ConnectionMode.Strict
? targetHandleBounds!.target
: (targetHandleBounds!.target ?? []).concat(targetHandleBounds!.source ?? []);
const sourceHandle = getHandle(sourceHandleBounds!.source!, params.sourceHandle);
const targetHandle = getHandle(targetNodeHandles!, params.targetHandle);
const sourcePosition = sourceHandle?.position || Position.Bottom;
const targetPosition = targetHandle?.position || Position.Top;
if (!sourceHandle || !targetHandle) {
params.onError?.('008', errorMessages['error008'](sourceHandle, {} as Edge));
return null;
}
const { sourceX, sourceY, targetX, targetY } = getEdgePositions(
sourceNodeRect,
sourceHandle,
sourcePosition,
targetNodeRect,
targetHandle,
targetPosition
);
return {
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
};
}
function useVisibleEdges(onlyRenderVisible: boolean, elevateEdgesOnSelect: boolean): GroupedEdges[] {
const edges = useStore(
useCallback(
(s: ReactFlowState) => {
if (!onlyRenderVisible) {
return s.edges;
}
const visibleEdges = onlyRenderVisible
? s.edges.filter((e) => {
const sourceNode = s.nodeInternals.get(e.source);
const targetNode = s.nodeInternals.get(e.target);
return s.edges.filter((e) => {
const sourceNode = nodeInternals.get(e.source);
const targetNode = nodeInternals.get(e.target);
return (
sourceNode?.width &&
sourceNode?.height &&
targetNode?.width &&
targetNode?.height &&
isEdgeVisible({
sourcePos: sourceNode.positionAbsolute || { x: 0, y: 0 },
targetPos: targetNode.positionAbsolute || { x: 0, y: 0 },
sourceWidth: sourceNode.width,
sourceHeight: sourceNode.height,
targetWidth: targetNode.width,
targetHeight: targetNode.height,
width: s.width,
height: s.height,
transform: s.transform,
return (
sourceNode?.width &&
sourceNode?.height &&
targetNode?.width &&
targetNode?.height &&
isEdgeVisible({
sourcePos: sourceNode.positionAbsolute || { x: 0, y: 0 },
targetPos: targetNode.positionAbsolute || { x: 0, y: 0 },
sourceWidth: sourceNode.width,
sourceHeight: sourceNode.height,
targetWidth: targetNode.width,
targetHeight: targetNode.height,
width: s.width,
height: s.height,
transform: s.transform,
})
);
})
);
});
: s.edges;
return groupEdgesByZLevel(visibleEdges, s.nodeInternals, elevateEdgesOnSelect);
},
[onlyRenderVisible, nodeInternals]
)
[onlyRenderVisible, elevateEdgesOnSelect]
),
(groupA, groupB) => {
const unEqual = groupA.some(
(item, index) =>
item.isMaxLevel !== groupB[index].isMaxLevel ||
item.level !== groupB[index].level ||
!shallow(item.edges, groupB[index].edges)
);
return !unEqual;
}
);
return groupEdgesByZLevel(edges, nodeInternals, elevateEdgesOnSelect);
return edges;
}
export default useVisibleEdges;

View File

@@ -54,12 +54,6 @@ export type WrapEdgeProps<T = any> = Omit<Edge<T>, 'sourceHandle' | 'targetHandl
onEdgeDoubleClick?: EdgeMouseHandler;
sourceHandleId?: string | null;
targetHandleId?: string | null;
sourceX: number;
sourceY: number;
targetX: number;
targetY: number;
sourcePosition: Position;
targetPosition: Position;
onEdgeUpdate?: OnEdgeUpdateFunc;
onContextMenu?: EdgeMouseHandler;
onMouseEnter?: EdgeMouseHandler;
@@ -88,18 +82,8 @@ export type EdgeProps<T = any> = Pick<
Edge<T>,
'id' | 'animated' | 'data' | 'style' | 'selected' | 'source' | 'target'
> &
Pick<
WrapEdgeProps,
| 'sourceX'
| 'sourceY'
| 'targetX'
| 'targetY'
| 'sourcePosition'
| 'targetPosition'
| 'sourceHandleId'
| 'targetHandleId'
| 'interactionWidth'
> &
Pick<WrapEdgeProps, 'sourceHandleId' | 'targetHandleId' | 'interactionWidth'> &
EdgePosition &
EdgeLabelOptions & {
markerStart?: string;
markerEnd?: string;
@@ -140,3 +124,12 @@ export type ConnectionLineComponentProps = {
};
export type ConnectionLineComponent = ComponentType<ConnectionLineComponentProps>;
export type EdgePosition = {
sourceX: number;
sourceY: number;
targetX: number;
targetY: number;
sourcePosition: Position;
targetPosition: Position;
};

View File

@@ -90,7 +90,7 @@ function onPointerDown(
getTransform,
}: OnPointerDownParams
) {
// when react-flow is used inside a shadow root we can't use document
// when xyflow is used inside a shadow root we can't use document
const doc = getHostForElement(event.target as HTMLElement);
let autoPanId = 0;
let closestHandle: ConnectionHandle | null;