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, noPanClassName,
onError, onError,
}: EdgeWrapperProps): JSX.Element | null { }: 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 edgeType = edge.type || 'default';
let EdgeComponent = edgeTypes?.[edgeType] || builtinEdgeTypes[edgeType]; let EdgeComponent = edgeTypes?.[edgeType] || builtinEdgeTypes[edgeType];
@@ -18,7 +18,6 @@ import { handleNodeClick } from '../Nodes/utils';
import type { NodeWrapperProps } from '../../types'; import type { NodeWrapperProps } from '../../types';
import { arrowKeyDiffs, builtinNodeTypes } from './utils'; import { arrowKeyDiffs, builtinNodeTypes } from './utils';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
const NodeWrapper = ({ const NodeWrapper = ({
id, id,
onClick, onClick,
@@ -2,11 +2,11 @@ import { memo, ReactNode } from 'react';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import useVisibleEdgeIds from '../../hooks/useVisibleEdges'; import useVisibleEdgeIds from '../../hooks/useVisibleEdgeIds';
import MarkerDefinitions from './MarkerDefinitions'; import MarkerDefinitions from './MarkerDefinitions';
import { GraphViewProps } from '../GraphView'; import { GraphViewProps } from '../GraphView';
import type { ReactFlowState } from '../../types';
import EdgeWrapper from '../../components/EdgeWrapper'; import EdgeWrapper from '../../components/EdgeWrapper';
import type { ReactFlowState } from '../../types';
type EdgeRendererProps = Pick< type EdgeRendererProps = Pick<
GraphViewProps, GraphViewProps,
@@ -1,7 +1,7 @@
import { memo } from 'react'; import { memo } from 'react';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import useVisibleNodesIds from '../../hooks/useVisibleNodes'; import useVisibleNodesIds from '../../hooks/useVisibleNodeIds';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import { containerStyle } from '../../styles/utils'; import { containerStyle } from '../../styles/utils';
import { GraphViewProps } from '../GraphView'; 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 UserSelection from '../../components/UserSelection';
import { containerStyle } from '../../styles/utils'; import { containerStyle } from '../../styles/utils';
import { useStore, useStoreApi } from '../../hooks/useStore'; import { useStore, useStoreApi } from '../../hooks/useStore';
import { getSelectionChanges, getConnectedEdges } from '../../utils'; import { getSelectionChanges } from '../../utils';
import type { ReactFlowProps, ReactFlowState, NodeChange, EdgeChange } from '../../types'; import type { ReactFlowProps, ReactFlowState, NodeChange, EdgeChange } from '../../types';
type PaneProps = { type PaneProps = {
@@ -156,19 +156,30 @@ const Pane = memo(
true, true,
nodeOrigin nodeOrigin
); );
const selectedEdgeIds = getConnectedEdges(selectedNodes, edges).map((e) => e.id);
const selectedNodeIds = selectedNodes.map((n) => n.id);
if (prevSelectedNodesCount.current !== selectedNodeIds.length) { const selectedEdgeIds = new Set<string>();
prevSelectedNodesCount.current = selectedNodeIds.length; const selectedNodeIds = new Set<string>();
const changes = getSelectionChanges(nodes, selectedNodeIds) as NodeChange[];
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) { if (changes.length) {
onNodesChange?.(changes); onNodesChange?.(changes);
} }
} }
if (prevSelectedEdgesCount.current !== selectedEdgeIds.length) { if (prevSelectedEdgesCount.current !== selectedEdgeIds.size) {
prevSelectedEdgesCount.current = selectedEdgeIds.length; prevSelectedEdgesCount.current = selectedEdgeIds.size;
const changes = getSelectionChanges(edges, selectedEdgeIds) as EdgeChange[]; const changes = getSelectionChanges(edges, selectedEdgeIds) as EdgeChange[];
if (changes.length) { if (changes.length) {
onEdgesChange?.(changes); 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 { getNodesInside } from '@xyflow/system';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import { useStore } from '../hooks/useStore'; import { useStore } from './useStore';
import type { Node, ReactFlowState } from '../types'; import type { Node, ReactFlowState } from '../types';
import { useCallback } from 'react'; import { useCallback } from 'react';
+7 -8
View File
@@ -55,12 +55,11 @@ const createRFStore = ({
set({ nodes: nodesWithInternalData }); set({ nodes: nodesWithInternalData });
}, },
setEdges: (edges: Edge[]) => { setEdges: (edges: Edge[]) => {
const { defaultEdgeOptions = {}, connectionLookup, edgeLookup } = get(); const { connectionLookup, edgeLookup } = get();
const nextEdges = edges.map((e) => ({ ...defaultEdgeOptions, ...e }));
updateConnectionLookup(connectionLookup, edgeLookup, nextEdges); updateConnectionLookup(connectionLookup, edgeLookup, edges);
set({ edges: nextEdges }); set({ edges });
}, },
// when the user works with an uncontrolled flow, // when the user works with an uncontrolled flow,
// we set a flag `hasDefaultNodes` / `hasDefaultEdges` // we set a flag `hasDefaultNodes` / `hasDefaultEdges`
@@ -196,8 +195,8 @@ const createRFStore = ({
if (multiSelectionActive) { if (multiSelectionActive) {
changedNodes = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true)) as NodeSelectionChange[]; changedNodes = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true)) as NodeSelectionChange[];
} else { } else {
changedNodes = getSelectionChanges(nodes, selectedNodeIds); changedNodes = getSelectionChanges(nodes, new Set([...selectedNodeIds]), true);
changedEdges = getSelectionChanges(edges, []); changedEdges = getSelectionChanges(edges);
} }
updateNodesAndEdgesSelections({ updateNodesAndEdgesSelections({
@@ -215,8 +214,8 @@ const createRFStore = ({
if (multiSelectionActive) { if (multiSelectionActive) {
changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true)) as EdgeSelectionChange[]; changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true)) as EdgeSelectionChange[];
} else { } else {
changedEdges = getSelectionChanges(edges, selectedEdgeIds); changedEdges = getSelectionChanges(edges, new Set([...selectedEdgeIds]));
changedNodes = getSelectionChanges(nodes, []); changedNodes = getSelectionChanges(nodes, new Set(), true);
} }
updateNodesAndEdgesSelections({ updateNodesAndEdgesSelections({
+1 -1
View File
@@ -49,7 +49,7 @@ const getInitialState = ({
transform, transform,
nodes: nextNodes, nodes: nextNodes,
nodeLookup, nodeLookup,
edges: edges, edges,
edgeLookup, edgeLookup,
connectionLookup, connectionLookup,
onNodesChange: null, onNodesChange: null,
+1 -2
View File
@@ -34,9 +34,8 @@ export type EdgeUpdatable = boolean | HandleType;
export type DefaultEdge<EdgeData = any> = EdgeBase<EdgeData> & { export type DefaultEdge<EdgeData = any> = EdgeBase<EdgeData> & {
style?: CSSProperties; style?: CSSProperties;
className?: string; className?: string;
sourceNode?: Node;
targetNode?: Node;
updatable?: EdgeUpdatable; updatable?: EdgeUpdatable;
focusable?: boolean;
} & EdgeLabelOptions; } & EdgeLabelOptions;
type SmoothStepEdgeType<T> = DefaultEdge<T> & { 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 type { CoordinateExtent, NodeBase, NodeOrigin, OnError } from '@xyflow/system';
import { NodeTypes } from './general'; 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< export type Node<NodeData = any, NodeType extends string | undefined = string | undefined> = NodeBase<
NodeData, NodeData,
NodeType NodeType
-1
View File
@@ -54,7 +54,6 @@ export type ReactFlowStore = {
edges: Edge[]; edges: Edge[];
edgeLookup: EdgeLookup<Edge>; edgeLookup: EdgeLookup<Edge>;
connectionLookup: ConnectionLookup; connectionLookup: ConnectionLookup;
onNodesChange: OnNodesChange | null; onNodesChange: OnNodesChange | null;
onEdgesChange: OnEdgesChange | null; onEdgesChange: OnEdgesChange | null;
hasDefaultNodes: boolean; hasDefaultNodes: boolean;
+21 -13
View File
@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* 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) { export function handleParentExpand(res: any[], updateItem: any) {
const parent = res.find((e) => e.id === updateItem.parentNode); 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>[]; return applyChanges(changes, edges) as Edge<EdgeData>[];
} }
export const createSelectionChange = (id: string, selected: boolean) => ({ export const createSelectionChange = (id: string, selected: boolean): NodeSelectionChange | EdgeSelectionChange => ({
id, id,
type: 'select', type: 'select',
selected, selected,
}); });
export function getSelectionChanges(items: any[], selectedIds: string[]) { export function getSelectionChanges(
return items.reduce((res, item) => { items: any[],
const willBeSelected = selectedIds.includes(item.id); selectedIds: Set<string> = new Set(),
mutateItem = false
): NodeSelectionChange[] | EdgeSelectionChange[] {
const changes: NodeSelectionChange[] | EdgeSelectionChange[] = [];
if (!item.selected && willBeSelected) { for (const item of items) {
item.selected = true; const willBeSelected = selectedIds.has(item.id);
res.push(createSelectionChange(item.id, true));
} else if (item.selected && !willBeSelected) { if (item.selected !== willBeSelected) {
item.selected = false; if (mutateItem) {
res.push(createSelectionChange(item.id, false)); // 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'; import type { Node } from '$lib/types';
export type DefaultEdge<EdgeData = any> = Omit<EdgeBase<EdgeData>, 'focusable'> & { export type DefaultEdge<EdgeData = any> = EdgeBase<EdgeData> & {
label?: string; label?: string;
labelStyle?: string; labelStyle?: string;
style?: string; style?: string;
@@ -75,7 +75,7 @@ export type StraightEdgeProps<T = any> = Omit<
export type EdgeTypes = Record<string, ComponentType<SvelteComponent<EdgeProps>>>; export type EdgeTypes = Record<string, ComponentType<SvelteComponent<EdgeProps>>>;
export type DefaultEdgeOptions = Omit<DefaultEdgeOptionsBase<Edge>, 'focusable'>; export type DefaultEdgeOptions = DefaultEdgeOptionsBase<Edge>;
export type EdgeLayouted = Pick< export type EdgeLayouted = Pick<
Edge, Edge,
+1 -2
View File
@@ -19,7 +19,6 @@ export type EdgeBase<EdgeData = any> = {
zIndex?: number; zIndex?: number;
ariaLabel?: string; ariaLabel?: string;
interactionWidth?: number; interactionWidth?: number;
focusable?: boolean;
}; };
export type SmoothStepPathOptions = { export type SmoothStepPathOptions = {
@@ -37,7 +36,7 @@ export type BezierPathOptions = {
export type DefaultEdgeOptionsBase<EdgeType extends EdgeBase> = Omit< export type DefaultEdgeOptionsBase<EdgeType extends EdgeBase> = Omit<
EdgeType, EdgeType,
'id' | 'source' | 'target' | 'sourceHandle' | 'targetHandle' | 'sourceNode' | 'targetNode' 'id' | 'source' | 'target' | 'sourceHandle' | 'targetHandle' | 'selected'
>; >;
export enum ConnectionLineType { export enum ConnectionLineType {
+14 -14
View File
@@ -43,10 +43,13 @@ export function updateAbsolutePositions<NodeType extends NodeBase>(
parentNode?.origin || nodeOrigin parentNode?.origin || nodeOrigin
); );
node.computed!.positionAbsolute = { const positionChanged = x !== node.computed?.positionAbsolute?.x || y !== node.computed?.positionAbsolute?.y;
x, node.computed!.positionAbsolute = positionChanged
y, ? {
}; x,
y,
}
: node.computed?.positionAbsolute;
node[internalsSymbol]!.z = z; node[internalsSymbol]!.z = z;
@@ -245,25 +248,22 @@ export function panBy({
return transformChanged; return transformChanged;
} }
export function updateConnectionLookup(lookup: ConnectionLookup, edgeLookup: EdgeLookup, edges: EdgeBase[]) { export function updateConnectionLookup(connectionLookup: ConnectionLookup, edgeLookup: EdgeLookup, edges: EdgeBase[]) {
lookup.clear(); connectionLookup.clear();
edgeLookup.clear(); edgeLookup.clear();
for (const edge of edges) { for (const edge of edges) {
edgeLookup.set(edge.id, edge);
const { source, target, sourceHandle = null, targetHandle = null } = edge; const { source, target, sourceHandle = null, targetHandle = null } = edge;
const sourceKey = `${source}-source-${sourceHandle}`; const sourceKey = `${source}-source-${sourceHandle}`;
const targetKey = `${target}-target-${targetHandle}`; const targetKey = `${target}-target-${targetHandle}`;
const prevSource = lookup.get(sourceKey) || new Map(); const prevSource = connectionLookup.get(sourceKey) || new Map();
const prevTarget = lookup.get(targetKey) || new Map(); const prevTarget = connectionLookup.get(targetKey) || new Map();
const connection = { source, target, sourceHandle, targetHandle }; const connection = { source, target, sourceHandle, targetHandle };
lookup.set(sourceKey, prevSource.set(`${target}-${targetHandle}`, connection)); edgeLookup.set(edge.id, edge);
lookup.set(targetKey, prevTarget.set(`${source}-${sourceHandle}`, connection)); connectionLookup.set(sourceKey, prevSource.set(`${target}-${targetHandle}`, connection));
connectionLookup.set(targetKey, prevTarget.set(`${source}-${sourceHandle}`, connection));
} }
return lookup;
} }