refactor(applyChanges): use specific replace events instead of a rough reset

This commit is contained in:
moklick
2024-01-11 16:03:02 +01:00
parent 51c22c804e
commit 2ba35de186
6 changed files with 145 additions and 61 deletions
+41 -41
View File
@@ -8,14 +8,12 @@ import type {
Instance,
NodeAddChange,
EdgeAddChange,
NodeResetChange,
EdgeResetChange,
NodeRemoveChange,
EdgeRemoveChange,
Node,
Edge,
NodeChange,
EdgeChange,
} from '../types';
import { isNode } from '../utils';
import { getElementsDiffChanges, isNode } from '../utils';
/**
* Hook for accessing the ReactFlow instance.
@@ -30,10 +28,6 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
const viewportHelper = useViewportHelper();
const store = useStoreApi();
// this is used to handle multiple syncronous setNodes calls
const setNodesData = useRef<Node[]>();
const setNodesTimeout = useRef<ReturnType<typeof setTimeout>>();
const getNodes = useCallback<Instance.GetNodes<NodeType>>(() => {
return store.getState().nodes.map((n) => ({ ...n })) as NodeType[];
}, []);
@@ -52,48 +46,54 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
return edges.find((e) => e.id === id) as EdgeType;
}, []);
// this is used to handle multiple syncronous setNodes calls
const setNodesData = useRef<Node[]>();
const setNodesTimeout = useRef<ReturnType<typeof setTimeout>>();
const setNodes = useCallback<Instance.SetNodes<NodeType>>((payload) => {
const { nodes, setNodes, hasDefaultNodes, onNodesChange } = store.getState();
setNodesData.current = setNodesData.current || nodes;
const nextNodes = typeof payload === 'function' ? payload(setNodesData.current as NodeType[]) : payload;
const { nodes = [], setNodes, hasDefaultNodes, onNodesChange, nodeLookup } = store.getState();
const nextNodes = typeof payload === 'function' ? payload((setNodesData.current as NodeType[]) || nodes) : payload;
setNodesData.current = nextNodes;
if (hasDefaultNodes) {
setNodes(nextNodes);
} else if (onNodesChange) {
if (setNodesTimeout.current) {
clearTimeout(setNodesTimeout.current);
}
// if there are multiple synchronous setNodes calls, we only want to call onNodesChange once
// for this, we use a timeout to wait for the last call and store updated nodes in setNodesData
// this is not perfect, but should work in most cases
setNodesTimeout.current = setTimeout(() => {
const changes =
nextNodes.length === 0
? nodes.map((node) => ({ type: 'remove', id: node.id } as NodeRemoveChange))
: nextNodes.map((node) => ({ item: node, type: 'reset' } as NodeResetChange<NodeType>));
onNodesChange(changes);
setNodesData.current = undefined;
}, 0);
if (setNodesTimeout.current) {
clearTimeout(setNodesTimeout.current);
}
// if there are multiple synchronous setNodes calls, we only want to call onNodesChange once
// for this, we use a timeout to wait for the last call and store updated nodes in setNodesData
// this is not perfect, but should work in most cases
setNodesTimeout.current = setTimeout(() => {
if (hasDefaultNodes) {
setNodes(nextNodes);
} else if (onNodesChange) {
const changes: NodeChange[] = getElementsDiffChanges({ items: setNodesData.current, lookup: nodeLookup });
onNodesChange(changes);
setNodesData.current = undefined;
}
}, 0);
}, []);
// this is used to handle multiple syncronous setNodes calls
const setEdgesData = useRef<Edge[]>();
const setEdgesTimeout = useRef<ReturnType<typeof setTimeout>>();
const setEdges = useCallback<Instance.SetEdges<EdgeType>>((payload) => {
const { edges = [], setEdges, hasDefaultEdges, onEdgesChange } = store.getState();
const nextEdges = typeof payload === 'function' ? payload(edges as EdgeType[]) : payload;
const { edges = [], setEdges, hasDefaultEdges, onEdgesChange, edgeLookup } = store.getState();
const nextEdges = typeof payload === 'function' ? payload((setEdgesData.current as EdgeType[]) || edges) : payload;
if (hasDefaultEdges) {
setEdges(nextEdges);
} else if (onEdgesChange) {
const changes =
nextEdges.length === 0
? edges.map((edge) => ({ type: 'remove', id: edge.id } as EdgeRemoveChange))
: nextEdges.map((edge) => ({ item: edge, type: 'reset' } as EdgeResetChange<EdgeType>));
onEdgesChange(changes);
setEdgesData.current = nextEdges;
if (setEdgesTimeout.current) {
clearTimeout(setEdgesTimeout.current);
}
setEdgesTimeout.current = setTimeout(() => {
if (hasDefaultEdges) {
setEdges(nextEdges);
} else if (onEdgesChange) {
const changes: EdgeChange[] = getElementsDiffChanges({ items: nextEdges, lookup: edgeLookup });
onEdgesChange(changes);
}
}, 0);
}, []);
const addNodes = useCallback<Instance.AddNodes<NodeType>>((payload) => {
+9 -6
View File
@@ -34,9 +34,10 @@ export type NodeAddChange<NodeType extends Node = Node> = {
type: 'add';
};
export type NodeResetChange<NodeType extends Node = Node> = {
export type NodeReplaceChange<NodeType extends Node = Node> = {
id: string;
item: NodeType;
type: 'reset';
type: 'replace';
};
/**
@@ -49,7 +50,7 @@ export type NodeChange =
| NodeSelectionChange
| NodeRemoveChange
| NodeAddChange
| NodeResetChange;
| NodeReplaceChange;
export type EdgeSelectionChange = NodeSelectionChange;
export type EdgeRemoveChange = NodeRemoveChange;
@@ -57,8 +58,10 @@ export type EdgeAddChange<EdgeType extends Edge = Edge> = {
item: EdgeType;
type: 'add';
};
export type EdgeResetChange<EdgeType extends Edge = Edge> = {
export type EdgeReplaceChange<EdgeType extends Edge = Edge> = {
id: string;
item: EdgeType;
type: 'reset';
type: 'replace';
};
export type EdgeChange = EdgeSelectionChange | EdgeRemoveChange | EdgeAddChange | EdgeResetChange;
export type EdgeChange = EdgeSelectionChange | EdgeRemoveChange | EdgeAddChange | EdgeReplaceChange;
+63 -6
View File
@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { EdgeLookup, NodeLookup } from '@xyflow/system';
import type { Node, Edge, EdgeChange, NodeChange, NodeSelectionChange, EdgeSelectionChange } from '../types';
export function handleParentExpand(updatedElements: any[], updateItem: any) {
@@ -53,11 +54,6 @@ export function handleParentExpand(updatedElements: any[], updateItem: any) {
// When you drag a node for example, React Flow will send a position change update.
// This function then applies the changes and returns the updated elements.
function applyChanges(changes: any[], elements: any[]): any[] {
// we need this hack to handle the setNodes and setEdges function of the useReactFlow hook for controlled flows
if (changes.some((c) => c.type === 'reset')) {
return changes.filter((c) => c.type === 'reset').map((c) => c.item);
}
const updatedElements: any[] = [];
// By storing a map of changes for each element, we can a quick lookup as we
// iterate over the elements array!
@@ -67,7 +63,7 @@ function applyChanges(changes: any[], elements: any[]): any[] {
if (change.type === 'add') {
updatedElements.push(change.item);
continue;
} else if (change.type === 'remove') {
} else if (change.type === 'remove' || change.type === 'replace') {
// For a 'remove' change we can safely ignore any other changes queued for
// the same element, it's going to be removed anyway!
changesMap.set(change.id, [change]);
@@ -99,6 +95,11 @@ function applyChanges(changes: any[], elements: any[]): any[] {
continue;
}
if (changes[0].type === 'replace') {
updatedElements.push({ ...changes[0].item });
continue;
}
// For other types of changes, we want to start with a shallow copy of the
// object so React knows this element has changed. Sequential changes will
/// each _mutate_ this object, so there's only ever one copy.
@@ -245,3 +246,59 @@ export function getSelectionChanges(
return changes;
}
/**
* This function is used to find the changes between two sets of elements.
* It is used to determine which nodes or edges have been added, removed or replaced.
*
* @internal
* @param params.items = the next set of elements (nodes or edges)
* @param params.lookup = a lookup map of the current store elements
* @returns an array of changes
*/
export function getElementsDiffChanges({
items,
lookup,
}: {
items: Node[] | undefined;
lookup: NodeLookup;
}): NodeChange[];
export function getElementsDiffChanges({
items,
lookup,
}: {
items: Edge[] | undefined;
lookup: EdgeLookup;
}): EdgeChange[];
export function getElementsDiffChanges({
items = [],
lookup,
}: {
items: any[] | undefined;
lookup: Map<string, any>;
}): any[] {
const changes: any[] = [];
const itemsLookup = new Map<string, any>(items.map((item) => [item.id, item]));
for (const item of items) {
const storeItem = lookup.get(item.id);
if (storeItem !== undefined && storeItem !== item) {
changes.push({ id: item.id, item: item, type: 'replace' });
}
if (storeItem === undefined) {
changes.push({ item: item, type: 'add' });
}
}
for (const [id] of lookup) {
const nextNode = itemsLookup.get(id);
if (nextNode === undefined) {
changes.push({ id, type: 'remove' });
}
}
return changes;
}