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
@@ -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 cc from 'classcat';
import { getMarkerId, elementSelectionKeys, XYHandle, type Connection } from '@xyflow/system'; 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 { ARIA_EDGE_DESC_KEY } from '../A11yDescriptions';
import { EdgeAnchor } from './EdgeAnchor'; import { EdgeAnchor } from './EdgeAnchor';
import { getMouseHandler } from './utils'; import { getMouseHandler } from './utils';
import type { EdgeProps, WrapEdgeProps } from '../../types'; import type { EdgeProps, WrapEdgeProps } from '../../types';
import { getEdgePosition } from '../../hooks/useVisibleEdges';
const alwaysValidConnection = () => true; import { shallow } from 'zustand/shallow';
export default (EdgeComponent: ComponentType<EdgeProps>) => { export default (EdgeComponent: ComponentType<EdgeProps>) => {
const EdgeWrapper = ({ const EdgeWrapper = ({
@@ -29,12 +29,6 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
style, style,
source, source,
target, target,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
isSelectable, isSelectable,
hidden, hidden,
sourceHandleId, sourceHandleId,
@@ -60,11 +54,34 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
const [updateHover, setUpdateHover] = useState<boolean>(false); const [updateHover, setUpdateHover] = useState<boolean>(false);
const [updating, setUpdating] = useState<boolean>(false); const [updating, setUpdating] = useState<boolean>(false);
const store = useStoreApi(); 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 markerStartUrl = useMemo(() => `url(#${getMarkerId(markerStart, rfId)})`, [markerStart, rfId]);
const markerEndUrl = useMemo(() => `url(#${getMarkerId(markerEnd, rfId)})`, [markerEnd, rfId]); const markerEndUrl = useMemo(() => `url(#${getMarkerId(markerEnd, rfId)})`, [markerEnd, rfId]);
if (hidden) { if (hidden || !edgePosition) {
return null; return null;
} }
@@ -211,12 +228,12 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
labelBgBorderRadius={labelBgBorderRadius} labelBgBorderRadius={labelBgBorderRadius}
data={data} data={data}
style={style} style={style}
sourceX={sourceX} sourceX={edgePosition.sourceX}
sourceY={sourceY} sourceY={edgePosition.sourceY}
targetX={targetX} targetX={edgePosition.targetX}
targetY={targetY} targetY={edgePosition.targetY}
sourcePosition={sourcePosition} sourcePosition={edgePosition.sourcePosition}
targetPosition={targetPosition} targetPosition={edgePosition.targetPosition}
sourceHandleId={sourceHandleId} sourceHandleId={sourceHandleId}
targetHandleId={targetHandleId} targetHandleId={targetHandleId}
markerStart={markerStartUrl} markerStart={markerStartUrl}
@@ -229,9 +246,9 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
<> <>
{(isUpdatable === 'source' || isUpdatable === true) && ( {(isUpdatable === 'source' || isUpdatable === true) && (
<EdgeAnchor <EdgeAnchor
position={sourcePosition} position={edgePosition.sourcePosition}
centerX={sourceX} centerX={edgePosition.sourceX}
centerY={sourceY} centerY={edgePosition.sourceY}
radius={edgeUpdaterRadius} radius={edgeUpdaterRadius}
onMouseDown={onEdgeUpdaterSourceMouseDown} onMouseDown={onEdgeUpdaterSourceMouseDown}
onMouseEnter={onEdgeUpdaterMouseEnter} onMouseEnter={onEdgeUpdaterMouseEnter}
@@ -241,9 +258,9 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
)} )}
{(isUpdatable === 'target' || isUpdatable === true) && ( {(isUpdatable === 'target' || isUpdatable === true) && (
<EdgeAnchor <EdgeAnchor
position={targetPosition} position={edgePosition.targetPosition}
centerX={targetX} centerX={edgePosition.targetX}
centerY={targetY} centerY={edgePosition.targetY}
radius={edgeUpdaterRadius} radius={edgeUpdaterRadius}
onMouseDown={onEdgeUpdaterTargetMouseDown} onMouseDown={onEdgeUpdaterTargetMouseDown}
onMouseEnter={onEdgeUpdaterMouseEnter} onMouseEnter={onEdgeUpdaterMouseEnter}
@@ -1,14 +1,13 @@
import { memo, ReactNode } from 'react'; import { memo, ReactNode } from 'react';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import cc from 'classcat'; import cc from 'classcat';
import { errorMessages, ConnectionMode, Position, getHandle } from '@xyflow/system'; import { errorMessages } from '@xyflow/system';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import useVisibleEdges from '../../hooks/useVisibleEdges'; import useVisibleEdges from '../../hooks/useVisibleEdges';
import MarkerDefinitions from './MarkerDefinitions'; import MarkerDefinitions from './MarkerDefinitions';
import { getEdgePositions, getNodeData } from './utils';
import { GraphViewProps } from '../GraphView'; import { GraphViewProps } from '../GraphView';
import type { Edge, ReactFlowState } from '../../types'; import type { ReactFlowState } from '../../types';
type EdgeRendererProps = Pick< type EdgeRendererProps = Pick<
GraphViewProps, GraphViewProps,
@@ -35,14 +34,12 @@ type EdgeRendererProps = Pick<
}; };
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
nodesConnectable: s.nodesConnectable, width: s.width,
height: s.height,
edgesFocusable: s.edgesFocusable, edgesFocusable: s.edgesFocusable,
edgesUpdatable: s.edgesUpdatable, edgesUpdatable: s.edgesUpdatable,
elementsSelectable: s.elementsSelectable, elementsSelectable: s.elementsSelectable,
width: s.width,
height: s.height,
connectionMode: s.connectionMode, connectionMode: s.connectionMode,
nodeInternals: s.nodeInternals,
onError: s.onError, onError: s.onError,
}); });
@@ -65,9 +62,8 @@ const EdgeRenderer = ({
onEdgeUpdateEnd, onEdgeUpdateEnd,
children, children,
}: EdgeRendererProps) => { }: EdgeRendererProps) => {
const { edgesFocusable, edgesUpdatable, elementsSelectable, width, height, connectionMode, nodeInternals, onError } = const { width, height, edgesFocusable, edgesUpdatable, elementsSelectable, onError } = useStore(selector, shallow);
useStore(selector, shallow); const edgeTree = useVisibleEdges(onlyRenderVisibleElements, elevateEdgesOnSelect);
const edgeTree = useVisibleEdges(onlyRenderVisibleElements, nodeInternals, elevateEdgesOnSelect);
if (!width) { if (!width) {
return null; return null;
@@ -85,14 +81,7 @@ const EdgeRenderer = ({
> >
{isMaxLevel && <MarkerDefinitions defaultColor={defaultMarkerColor} rfId={rfId} />} {isMaxLevel && <MarkerDefinitions defaultColor={defaultMarkerColor} rfId={rfId} />}
<g> <g>
{edges.map((edge: Edge) => { {edges.map((edge) => {
const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData(nodeInternals.get(edge.source));
const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(nodeInternals.get(edge.target));
if (!sourceIsValid || !targetIsValid) {
return null;
}
let edgeType = edge.type || 'default'; let edgeType = edge.type || 'default';
if (!edgeTypes[edgeType]) { if (!edgeTypes[edgeType]) {
@@ -100,16 +89,7 @@ const EdgeRenderer = ({
edgeType = 'default'; edgeType = 'default';
} }
const EdgeComponent = edgeTypes[edgeType] || edgeTypes.default; const EdgeComponent = edgeTypes[edgeType];
// 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 isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined')); const isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined'));
const isUpdatable = const isUpdatable =
typeof onEdgeUpdate !== 'undefined' && typeof onEdgeUpdate !== 'undefined' &&
@@ -119,27 +99,12 @@ const EdgeRenderer = ({
(elementsSelectable && typeof edge.selectable === 'undefined') (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 ( return (
<EdgeComponent <EdgeComponent
key={edge.id} key={edge.id}
id={edge.id} id={edge.id}
className={cc([edge.className, noPanClassName])} className={cc([edge.className, noPanClassName])}
type={edgeType} type={edge.type}
data={edge.data} data={edge.data}
selected={!!edge.selected} selected={!!edge.selected}
animated={!!edge.animated} animated={!!edge.animated}
@@ -157,12 +122,6 @@ const EdgeRenderer = ({
targetHandleId={edge.targetHandle} targetHandleId={edge.targetHandle}
markerEnd={edge.markerEnd} markerEnd={edge.markerEnd}
markerStart={edge.markerStart} markerStart={edge.markerStart}
sourceX={sourceX}
sourceY={sourceY}
targetX={targetX}
targetY={targetY}
sourcePosition={sourcePosition}
targetPosition={targetPosition}
isSelectable={isSelectable} isSelectable={isSelectable}
onEdgeUpdate={onEdgeUpdate} onEdgeUpdate={onEdgeUpdate}
onContextMenu={onEdgeContextMenu} onContextMenu={onEdgeContextMenu}
+110 -32
View File
@@ -1,13 +1,29 @@
import { useCallback } from 'react'; 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 { useStore } from '../hooks/useStore';
import { isEdgeVisible } from '../container/EdgeRenderer/utils'; import { getEdgePositions, getNodeData, isEdgeVisible } from '../container/EdgeRenderer/utils';
import { type ReactFlowState, type NodeInternals, type Edge } from '../types'; import { type ReactFlowState, type NodeInternals, type Edge, EdgePosition } from '../types';
import { shallow } from 'zustand/shallow';
const defaultEdgeTree = [{ level: 0, isMaxLevel: true, edges: [] }]; 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; let maxLevel = -1;
const levelLookup = edges.reduce<Record<string, Edge[]>>((tree, edge) => { const levelLookup = edges.reduce<Record<string, Edge[]>>((tree, edge) => {
@@ -51,42 +67,104 @@ function groupEdgesByZLevel(edges: Edge[], nodeInternals: NodeInternals, elevate
return edgeTree; 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( const edges = useStore(
useCallback( useCallback(
(s: ReactFlowState) => { (s: ReactFlowState) => {
if (!onlyRenderVisible) { const visibleEdges = onlyRenderVisible
return s.edges; ? s.edges.filter((e) => {
} const sourceNode = s.nodeInternals.get(e.source);
const targetNode = s.nodeInternals.get(e.target);
return s.edges.filter((e) => { return (
const sourceNode = nodeInternals.get(e.source); sourceNode?.width &&
const targetNode = nodeInternals.get(e.target); sourceNode?.height &&
targetNode?.width &&
return ( targetNode?.height &&
sourceNode?.width && isEdgeVisible({
sourceNode?.height && sourcePos: sourceNode.positionAbsolute || { x: 0, y: 0 },
targetNode?.width && targetPos: targetNode.positionAbsolute || { x: 0, y: 0 },
targetNode?.height && sourceWidth: sourceNode.width,
isEdgeVisible({ sourceHeight: sourceNode.height,
sourcePos: sourceNode.positionAbsolute || { x: 0, y: 0 }, targetWidth: targetNode.width,
targetPos: targetNode.positionAbsolute || { x: 0, y: 0 }, targetHeight: targetNode.height,
sourceWidth: sourceNode.width, width: s.width,
sourceHeight: sourceNode.height, height: s.height,
targetWidth: targetNode.width, transform: s.transform,
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; export default useVisibleEdges;
+11 -18
View File
@@ -54,12 +54,6 @@ export type WrapEdgeProps<T = any> = Omit<Edge<T>, 'sourceHandle' | 'targetHandl
onEdgeDoubleClick?: EdgeMouseHandler; onEdgeDoubleClick?: EdgeMouseHandler;
sourceHandleId?: string | null; sourceHandleId?: string | null;
targetHandleId?: string | null; targetHandleId?: string | null;
sourceX: number;
sourceY: number;
targetX: number;
targetY: number;
sourcePosition: Position;
targetPosition: Position;
onEdgeUpdate?: OnEdgeUpdateFunc; onEdgeUpdate?: OnEdgeUpdateFunc;
onContextMenu?: EdgeMouseHandler; onContextMenu?: EdgeMouseHandler;
onMouseEnter?: EdgeMouseHandler; onMouseEnter?: EdgeMouseHandler;
@@ -88,18 +82,8 @@ export type EdgeProps<T = any> = Pick<
Edge<T>, Edge<T>,
'id' | 'animated' | 'data' | 'style' | 'selected' | 'source' | 'target' 'id' | 'animated' | 'data' | 'style' | 'selected' | 'source' | 'target'
> & > &
Pick< Pick<WrapEdgeProps, 'sourceHandleId' | 'targetHandleId' | 'interactionWidth'> &
WrapEdgeProps, EdgePosition &
| 'sourceX'
| 'sourceY'
| 'targetX'
| 'targetY'
| 'sourcePosition'
| 'targetPosition'
| 'sourceHandleId'
| 'targetHandleId'
| 'interactionWidth'
> &
EdgeLabelOptions & { EdgeLabelOptions & {
markerStart?: string; markerStart?: string;
markerEnd?: string; markerEnd?: string;
@@ -140,3 +124,12 @@ export type ConnectionLineComponentProps = {
}; };
export type ConnectionLineComponent = ComponentType<ConnectionLineComponentProps>; export type ConnectionLineComponent = ComponentType<ConnectionLineComponentProps>;
export type EdgePosition = {
sourceX: number;
sourceY: number;
targetX: number;
targetY: number;
sourcePosition: Position;
targetPosition: Position;
};
+1 -1
View File
@@ -90,7 +90,7 @@ function onPointerDown(
getTransform, getTransform,
}: OnPointerDownParams }: 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); const doc = getHostForElement(event.target as HTMLElement);
let autoPanId = 0; let autoPanId = 0;
let closestHandle: ConnectionHandle | null; let closestHandle: ConnectionHandle | null;