refactor(react): dont create new edge objects on setEdges

This commit is contained in:
moklick
2023-12-18 17:03:48 +01:00
parent 32f0990e02
commit 4143591d96
17 changed files with 123 additions and 99 deletions
@@ -38,7 +38,9 @@ function EdgeWrapper({
noPanClassName,
onError,
}: EdgeWrapperProps): JSX.Element | null {
const edge = useStore((s) => s.edgeLookup.get(id)!);
let edge = useStore((s) => s.edgeLookup.get(id)!);
const defaultEdgeOptions = useStore((s) => s.defaultEdgeOptions);
edge = defaultEdgeOptions ? { ...defaultEdgeOptions, ...edge } : edge;
let edgeType = edge.type || 'default';
let EdgeComponent = edgeTypes?.[edgeType] || builtinEdgeTypes[edgeType];
@@ -18,7 +18,6 @@ import { handleNodeClick } from '../Nodes/utils';
import type { NodeWrapperProps } from '../../types';
import { arrowKeyDiffs, builtinNodeTypes } from './utils';
import { shallow } from 'zustand/shallow';
const NodeWrapper = ({
id,
onClick,
@@ -2,11 +2,11 @@ import { memo, ReactNode } from 'react';
import { shallow } from 'zustand/shallow';
import { useStore } from '../../hooks/useStore';
import useVisibleEdgeIds from '../../hooks/useVisibleEdges';
import useVisibleEdgeIds from '../../hooks/useVisibleEdgeIds';
import MarkerDefinitions from './MarkerDefinitions';
import { GraphViewProps } from '../GraphView';
import type { ReactFlowState } from '../../types';
import EdgeWrapper from '../../components/EdgeWrapper';
import type { ReactFlowState } from '../../types';
type EdgeRendererProps = Pick<
GraphViewProps,
@@ -1,7 +1,7 @@
import { memo } from 'react';
import { shallow } from 'zustand/shallow';
import useVisibleNodesIds from '../../hooks/useVisibleNodes';
import useVisibleNodesIds from '../../hooks/useVisibleNodeIds';
import { useStore } from '../../hooks/useStore';
import { containerStyle } from '../../styles/utils';
import { GraphViewProps } from '../GraphView';
+19 -8
View File
@@ -10,7 +10,7 @@ import { getNodesInside, getEventPosition, SelectionMode } from '@xyflow/system'
import UserSelection from '../../components/UserSelection';
import { containerStyle } from '../../styles/utils';
import { useStore, useStoreApi } from '../../hooks/useStore';
import { getSelectionChanges, getConnectedEdges } from '../../utils';
import { getSelectionChanges } from '../../utils';
import type { ReactFlowProps, ReactFlowState, NodeChange, EdgeChange } from '../../types';
type PaneProps = {
@@ -156,19 +156,30 @@ const Pane = memo(
true,
nodeOrigin
);
const selectedEdgeIds = getConnectedEdges(selectedNodes, edges).map((e) => e.id);
const selectedNodeIds = selectedNodes.map((n) => n.id);
if (prevSelectedNodesCount.current !== selectedNodeIds.length) {
prevSelectedNodesCount.current = selectedNodeIds.length;
const changes = getSelectionChanges(nodes, selectedNodeIds) as NodeChange[];
const selectedEdgeIds = new Set<string>();
const selectedNodeIds = new Set<string>();
for (const selectedNode of selectedNodes) {
selectedNodeIds.add(selectedNode.id);
for (const edge of edges) {
if (edge.source === selectedNode.id || edge.target === selectedNode.id) {
selectedEdgeIds.add(edge.id);
}
}
}
if (prevSelectedNodesCount.current !== selectedNodeIds.size) {
prevSelectedNodesCount.current = selectedNodeIds.size;
const changes = getSelectionChanges(nodes, selectedNodeIds, true) as NodeChange[];
if (changes.length) {
onNodesChange?.(changes);
}
}
if (prevSelectedEdgesCount.current !== selectedEdgeIds.length) {
prevSelectedEdgesCount.current = selectedEdgeIds.length;
if (prevSelectedEdgesCount.current !== selectedEdgeIds.size) {
prevSelectedEdgesCount.current = selectedEdgeIds.size;
const changes = getSelectionChanges(edges, selectedEdgeIds) as EdgeChange[];
if (changes.length) {
onEdgesChange?.(changes);
@@ -0,0 +1,49 @@
import { useCallback } from 'react';
import { shallow } from 'zustand/shallow';
import { isEdgeVisible } from '@xyflow/system';
import { useStore } from './useStore';
import { type ReactFlowState } from '../types';
function useVisibleEdgeIds(onlyRenderVisible: boolean): string[] {
const edgeIds = useStore(
useCallback(
(s: ReactFlowState) => {
if (!onlyRenderVisible) {
return s.edges.map((edge) => edge.id);
}
const visibleEdgeIds = [];
if (s.width && s.height) {
for (const edge of s.edges) {
const sourceNode = s.nodeLookup.get(edge.source);
const targetNode = s.nodeLookup.get(edge.target);
if (
sourceNode &&
targetNode &&
isEdgeVisible({
sourceNode,
targetNode,
width: s.width,
height: s.height,
transform: s.transform,
})
) {
visibleEdgeIds.push(edge.id);
}
}
}
return visibleEdgeIds;
},
[onlyRenderVisible]
),
shallow
);
return edgeIds;
}
export default useVisibleEdgeIds;
@@ -1,42 +0,0 @@
import { useCallback } from 'react';
import { shallow } from 'zustand/shallow';
import { isEdgeVisible } from '@xyflow/system';
import { useStore } from '../hooks/useStore';
import { type ReactFlowState } from '../types';
function useVisibleEdgeIds(onlyRenderVisible: boolean): string[] {
const edgeIds = useStore(
useCallback(
(s: ReactFlowState) => {
const visibleEdges =
onlyRenderVisible && s.width && s.height
? s.edges.filter((e) => {
const sourceNode = s.nodeLookup.get(e.source);
const targetNode = s.nodeLookup.get(e.target);
return (
sourceNode &&
targetNode &&
isEdgeVisible({
sourceNode,
targetNode,
width: s.width,
height: s.height,
transform: s.transform,
})
);
})
: s.edges;
return visibleEdges.map((edge) => edge.id);
},
[onlyRenderVisible]
),
shallow
);
return edgeIds;
}
export default useVisibleEdgeIds;
@@ -1,7 +1,7 @@
import { getNodesInside } from '@xyflow/system';
import { shallow } from 'zustand/shallow';
import { useStore } from '../hooks/useStore';
import { useStore } from './useStore';
import type { Node, ReactFlowState } from '../types';
import { useCallback } from 'react';
+7 -8
View File
@@ -55,12 +55,11 @@ const createRFStore = ({
set({ nodes: nodesWithInternalData });
},
setEdges: (edges: Edge[]) => {
const { defaultEdgeOptions = {}, connectionLookup, edgeLookup } = get();
const nextEdges = edges.map((e) => ({ ...defaultEdgeOptions, ...e }));
const { connectionLookup, edgeLookup } = get();
updateConnectionLookup(connectionLookup, edgeLookup, nextEdges);
updateConnectionLookup(connectionLookup, edgeLookup, edges);
set({ edges: nextEdges });
set({ edges });
},
// when the user works with an uncontrolled flow,
// we set a flag `hasDefaultNodes` / `hasDefaultEdges`
@@ -196,8 +195,8 @@ const createRFStore = ({
if (multiSelectionActive) {
changedNodes = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true)) as NodeSelectionChange[];
} else {
changedNodes = getSelectionChanges(nodes, selectedNodeIds);
changedEdges = getSelectionChanges(edges, []);
changedNodes = getSelectionChanges(nodes, new Set([...selectedNodeIds]), true);
changedEdges = getSelectionChanges(edges);
}
updateNodesAndEdgesSelections({
@@ -215,8 +214,8 @@ const createRFStore = ({
if (multiSelectionActive) {
changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true)) as EdgeSelectionChange[];
} else {
changedEdges = getSelectionChanges(edges, selectedEdgeIds);
changedNodes = getSelectionChanges(nodes, []);
changedEdges = getSelectionChanges(edges, new Set([...selectedEdgeIds]));
changedNodes = getSelectionChanges(nodes, new Set(), true);
}
updateNodesAndEdgesSelections({
+1 -1
View File
@@ -49,7 +49,7 @@ const getInitialState = ({
transform,
nodes: nextNodes,
nodeLookup,
edges: edges,
edges,
edgeLookup,
connectionLookup,
onNodesChange: null,
+1 -2
View File
@@ -34,9 +34,8 @@ export type EdgeUpdatable = boolean | HandleType;
export type DefaultEdge<EdgeData = any> = EdgeBase<EdgeData> & {
style?: CSSProperties;
className?: string;
sourceNode?: Node;
targetNode?: Node;
updatable?: EdgeUpdatable;
focusable?: boolean;
} & EdgeLabelOptions;
type SmoothStepEdgeType<T> = DefaultEdge<T> & {
+1
View File
@@ -2,6 +2,7 @@ import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
import type { CoordinateExtent, NodeBase, NodeOrigin, OnError } from '@xyflow/system';
import { NodeTypes } from './general';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Node<NodeData = any, NodeType extends string | undefined = string | undefined> = NodeBase<
NodeData,
NodeType
-1
View File
@@ -54,7 +54,6 @@ export type ReactFlowStore = {
edges: Edge[];
edgeLookup: EdgeLookup<Edge>;
connectionLookup: ConnectionLookup;
onNodesChange: OnNodesChange | null;
onEdgesChange: OnEdgesChange | null;
hasDefaultNodes: boolean;
+21 -13
View File
@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Node, Edge, EdgeChange, NodeChange } from '../types';
import type { Node, Edge, EdgeChange, NodeChange, NodeSelectionChange, EdgeSelectionChange } from '../types';
export function handleParentExpand(res: any[], updateItem: any) {
const parent = res.find((e) => e.id === updateItem.parentNode);
@@ -150,24 +150,32 @@ export function applyEdgeChanges<EdgeData = any>(changes: EdgeChange[], edges: E
return applyChanges(changes, edges) as Edge<EdgeData>[];
}
export const createSelectionChange = (id: string, selected: boolean) => ({
export const createSelectionChange = (id: string, selected: boolean): NodeSelectionChange | EdgeSelectionChange => ({
id,
type: 'select',
selected,
});
export function getSelectionChanges(items: any[], selectedIds: string[]) {
return items.reduce((res, item) => {
const willBeSelected = selectedIds.includes(item.id);
export function getSelectionChanges(
items: any[],
selectedIds: Set<string> = new Set(),
mutateItem = false
): NodeSelectionChange[] | EdgeSelectionChange[] {
const changes: NodeSelectionChange[] | EdgeSelectionChange[] = [];
if (!item.selected && willBeSelected) {
item.selected = true;
res.push(createSelectionChange(item.id, true));
} else if (item.selected && !willBeSelected) {
item.selected = false;
res.push(createSelectionChange(item.id, false));
for (const item of items) {
const willBeSelected = selectedIds.has(item.id);
if (item.selected !== willBeSelected) {
if (mutateItem) {
// this hack is needed for nodes. When the user dragged a node, it's selected.
// When another node gets dragged, we need to deselect the previous one,
// in order to have only one selected node at a time - the onNodesChange callback comes too late here :/
item.selected = willBeSelected;
}
changes.push(createSelectionChange(item.id, willBeSelected));
}
}
return res;
}, []);
return changes;
}
+2 -2
View File
@@ -12,7 +12,7 @@ import type {
import type { Node } from '$lib/types';
export type DefaultEdge<EdgeData = any> = Omit<EdgeBase<EdgeData>, 'focusable'> & {
export type DefaultEdge<EdgeData = any> = EdgeBase<EdgeData> & {
label?: string;
labelStyle?: string;
style?: string;
@@ -75,7 +75,7 @@ export type StraightEdgeProps<T = any> = Omit<
export type EdgeTypes = Record<string, ComponentType<SvelteComponent<EdgeProps>>>;
export type DefaultEdgeOptions = Omit<DefaultEdgeOptionsBase<Edge>, 'focusable'>;
export type DefaultEdgeOptions = DefaultEdgeOptionsBase<Edge>;
export type EdgeLayouted = Pick<
Edge,
+1 -2
View File
@@ -19,7 +19,6 @@ export type EdgeBase<EdgeData = any> = {
zIndex?: number;
ariaLabel?: string;
interactionWidth?: number;
focusable?: boolean;
};
export type SmoothStepPathOptions = {
@@ -37,7 +36,7 @@ export type BezierPathOptions = {
export type DefaultEdgeOptionsBase<EdgeType extends EdgeBase> = Omit<
EdgeType,
'id' | 'source' | 'target' | 'sourceHandle' | 'targetHandle' | 'sourceNode' | 'targetNode'
'id' | 'source' | 'target' | 'sourceHandle' | 'targetHandle' | 'selected'
>;
export enum ConnectionLineType {
+14 -14
View File
@@ -43,10 +43,13 @@ export function updateAbsolutePositions<NodeType extends NodeBase>(
parentNode?.origin || nodeOrigin
);
node.computed!.positionAbsolute = {
x,
y,
};
const positionChanged = x !== node.computed?.positionAbsolute?.x || y !== node.computed?.positionAbsolute?.y;
node.computed!.positionAbsolute = positionChanged
? {
x,
y,
}
: node.computed?.positionAbsolute;
node[internalsSymbol]!.z = z;
@@ -245,25 +248,22 @@ export function panBy({
return transformChanged;
}
export function updateConnectionLookup(lookup: ConnectionLookup, edgeLookup: EdgeLookup, edges: EdgeBase[]) {
lookup.clear();
export function updateConnectionLookup(connectionLookup: ConnectionLookup, edgeLookup: EdgeLookup, edges: EdgeBase[]) {
connectionLookup.clear();
edgeLookup.clear();
for (const edge of edges) {
edgeLookup.set(edge.id, edge);
const { source, target, sourceHandle = null, targetHandle = null } = edge;
const sourceKey = `${source}-source-${sourceHandle}`;
const targetKey = `${target}-target-${targetHandle}`;
const prevSource = lookup.get(sourceKey) || new Map();
const prevTarget = lookup.get(targetKey) || new Map();
const prevSource = connectionLookup.get(sourceKey) || new Map();
const prevTarget = connectionLookup.get(targetKey) || new Map();
const connection = { source, target, sourceHandle, targetHandle };
lookup.set(sourceKey, prevSource.set(`${target}-${targetHandle}`, connection));
lookup.set(targetKey, prevTarget.set(`${source}-${sourceHandle}`, connection));
edgeLookup.set(edge.id, edge);
connectionLookup.set(sourceKey, prevSource.set(`${target}-${targetHandle}`, connection));
connectionLookup.set(targetKey, prevTarget.set(`${source}-${sourceHandle}`, connection));
}
return lookup;
}