Store: don’t regenerate internal nodes if user-provided nodes haven’t changed

This commit is contained in:
Ivan Akulov
2023-11-25 02:06:43 +01:00
parent 8fe8f73ed3
commit 24390162e0
6 changed files with 26 additions and 14 deletions
+12 -7
View File
@@ -2,7 +2,7 @@ import { createWithEqualityFn } from 'zustand/traditional';
import { import {
clampPosition, clampPosition,
fitView as fitViewSystem, fitView as fitViewSystem,
updateNodes, adoptUserProvidedNodes,
updateAbsolutePositions, updateAbsolutePositions,
panBy as panBySystem, panBy as panBySystem,
Dimensions, Dimensions,
@@ -42,11 +42,16 @@ const createRFStore = ({
...getInitialState({ nodes, edges, width, height, fitView }), ...getInitialState({ nodes, edges, width, height, fitView }),
setNodes: (nodes: Node[]) => { setNodes: (nodes: Node[]) => {
const { nodeLookup, nodeOrigin, elevateNodesOnSelect } = get(); const { nodeLookup, nodeOrigin, elevateNodesOnSelect } = get();
// Whenver new nodes are set, we need to calculate the absolute positions of the nodes // setNodes() is called exclusively in response to user actions:
// and update the nodeLookup. // - either when the `<ReactFlow nodes>` prop is updated in the controlled ReactFlow setup,
const nextNodes = updateNodes(nodes, nodeLookup, { nodeOrigin, elevateNodesOnSelect }); // - or when the user calls something like `reactFlowInstance.setNodes()` in an uncontrolled ReactFlow setup.
//
// When this happens, we take the note objects passed by the user and extend them with fields
// relevant for internal React Flow operations.
// TODO: consider updating the types to reflect the distinction between user-provided nodes and internal nodes.
const nodesWithInternalData = adoptUserProvidedNodes(nodes, nodeLookup, { nodeOrigin, elevateNodesOnSelect });
set({ nodes: nextNodes }); set({ nodes: nodesWithInternalData });
}, },
setEdges: (edges: Edge[]) => { setEdges: (edges: Edge[]) => {
const { defaultEdgeOptions = {} } = get(); const { defaultEdgeOptions = {} } = get();
@@ -69,7 +74,7 @@ const createRFStore = ({
}; };
if (hasDefaultNodes) { if (hasDefaultNodes) {
nextState.nodes = updateNodes(nodes, new Map(), { nextState.nodes = adoptUserProvidedNodes(nodes, new Map(), {
nodeOrigin: get().nodeOrigin, nodeOrigin: get().nodeOrigin,
elevateNodesOnSelect: get().elevateNodesOnSelect, elevateNodesOnSelect: get().elevateNodesOnSelect,
}); });
@@ -163,7 +168,7 @@ const createRFStore = ({
if (changes?.length) { if (changes?.length) {
if (hasDefaultNodes) { if (hasDefaultNodes) {
const updatedNodes = applyNodeChanges(changes, nodes); const updatedNodes = applyNodeChanges(changes, nodes);
const nextNodes = updateNodes(updatedNodes, nodeLookup, { const nextNodes = adoptUserProvidedNodes(updatedNodes, nodeLookup, {
nodeOrigin, nodeOrigin,
elevateNodesOnSelect, elevateNodesOnSelect,
}); });
+2 -2
View File
@@ -1,7 +1,7 @@
import { import {
infiniteExtent, infiniteExtent,
ConnectionMode, ConnectionMode,
updateNodes, adoptUserProvidedNodes,
getNodesBounds, getNodesBounds,
getViewportForBounds, getViewportForBounds,
Transform, Transform,
@@ -23,7 +23,7 @@ const getInitialState = ({
fitView?: boolean; fitView?: boolean;
} = {}): ReactFlowStore => { } = {}): ReactFlowStore => {
const nodeLookup = new Map<string, Node>(); const nodeLookup = new Map<string, Node>();
const nextNodes = updateNodes(nodes, nodeLookup, { nodeOrigin: [0, 0], elevateNodesOnSelect: false }); const nextNodes = adoptUserProvidedNodes(nodes, nodeLookup, { nodeOrigin: [0, 0], elevateNodesOnSelect: false });
let transform: Transform = [0, 0, 1]; let transform: Transform = [0, 0, 1];
@@ -15,7 +15,7 @@ import {
type OnError, type OnError,
devWarn, devWarn,
type Viewport, type Viewport,
updateNodes, adoptUserProvidedNodes,
getNodesBounds, getNodesBounds,
getViewportForBounds getViewportForBounds
} from '@xyflow/system'; } from '@xyflow/system';
@@ -68,7 +68,7 @@ export const getInitialStore = ({
fitView?: boolean; fitView?: boolean;
}) => { }) => {
const nodeLookup = new Map<string, Node>(); const nodeLookup = new Map<string, Node>();
const nextNodes = updateNodes(nodes, nodeLookup, { const nextNodes = adoptUserProvidedNodes(nodes, nodeLookup, {
nodeOrigin: [0, 0], nodeOrigin: [0, 0],
elevateNodesOnSelect: false elevateNodesOnSelect: false
}); });
+2 -2
View File
@@ -6,7 +6,7 @@ import {
type Writable, type Writable,
get get
} from 'svelte/store'; } from 'svelte/store';
import { updateNodes, type Viewport, type PanZoomInstance } from '@xyflow/system'; import { adoptUserProvidedNodes, type Viewport, type PanZoomInstance } from '@xyflow/system';
import type { DefaultEdgeOptions, DefaultNodeOptions, Edge, Node } from '$lib/types'; import type { DefaultEdgeOptions, DefaultNodeOptions, Edge, Node } from '$lib/types';
@@ -133,7 +133,7 @@ export const createNodesStore = (
let elevateNodesOnSelect = true; let elevateNodesOnSelect = true;
const _set = (nds: Node[]): Node[] => { const _set = (nds: Node[]): Node[] => {
const nextNodes = updateNodes(nds, nodeLookup, { const nextNodes = adoptUserProvidedNodes(nds, nodeLookup, {
elevateNodesOnSelect, elevateNodesOnSelect,
defaults defaults
}); });
+4
View File
@@ -40,6 +40,10 @@ export type NodeBase<T = any, U extends string | undefined = string | undefined>
z?: number; z?: number;
handleBounds?: NodeHandleBounds; handleBounds?: NodeHandleBounds;
isParent?: boolean; isParent?: boolean;
/** Holds a reference to the original node object provided by the user
* (which may lack some fields, like `computed` or `[internalSymbol]`. Used
* as an optimization to avoid certain operations. */
userProvidedNode: WeakRef<NodeBase>;
}; };
}; };
+4 -1
View File
@@ -62,7 +62,7 @@ type UpdateNodesOptions<NodeType extends NodeBase> = {
defaults?: Partial<NodeType>; defaults?: Partial<NodeType>;
}; };
export function updateNodes<NodeType extends NodeBase>( export function adoptUserProvidedNodes<NodeType extends NodeBase>(
nodes: NodeType[], nodes: NodeType[],
nodeLookup: Map<string, NodeType>, nodeLookup: Map<string, NodeType>,
options: UpdateNodesOptions<NodeType> = { options: UpdateNodesOptions<NodeType> = {
@@ -76,6 +76,8 @@ export function updateNodes<NodeType extends NodeBase>(
const nextNodes = nodes.map((n) => { const nextNodes = nodes.map((n) => {
const currentStoreNode = nodeLookup.get(n.id); const currentStoreNode = nodeLookup.get(n.id);
if (n === currentStoreNode?.[internalsSymbol]?.userProvidedNode.deref()) return currentStoreNode;
const node: NodeType = { const node: NodeType = {
...options.defaults, ...options.defaults,
...n, ...n,
@@ -97,6 +99,7 @@ export function updateNodes<NodeType extends NodeBase>(
value: { value: {
handleBounds: currInternals?.handleBounds, handleBounds: currInternals?.handleBounds,
z, z,
userProvidedNode: new WeakRef(n),
}, },
}); });