refactor(react): dont create new edge objects on setEdges
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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
-1
@@ -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';
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -49,7 +49,7 @@ const getInitialState = ({
|
||||
transform,
|
||||
nodes: nextNodes,
|
||||
nodeLookup,
|
||||
edges: edges,
|
||||
edges,
|
||||
edgeLookup,
|
||||
connectionLookup,
|
||||
onNodesChange: null,
|
||||
|
||||
@@ -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> & {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -54,7 +54,6 @@ export type ReactFlowStore = {
|
||||
edges: Edge[];
|
||||
edgeLookup: EdgeLookup<Edge>;
|
||||
connectionLookup: ConnectionLookup;
|
||||
|
||||
onNodesChange: OnNodesChange | null;
|
||||
onEdgesChange: OnEdgesChange | null;
|
||||
hasDefaultNodes: boolean;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user