diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 0f50f385..49c7c688 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -2,7 +2,7 @@ import { createWithEqualityFn } from 'zustand/traditional'; import { clampPosition, fitView as fitViewSystem, - updateNodes, + adoptUserProvidedNodes, updateAbsolutePositions, panBy as panBySystem, Dimensions, @@ -42,11 +42,16 @@ const createRFStore = ({ ...getInitialState({ nodes, edges, width, height, fitView }), setNodes: (nodes: Node[]) => { const { nodeLookup, nodeOrigin, elevateNodesOnSelect } = get(); - // Whenver new nodes are set, we need to calculate the absolute positions of the nodes - // and update the nodeLookup. - const nextNodes = updateNodes(nodes, nodeLookup, { nodeOrigin, elevateNodesOnSelect }); + // setNodes() is called exclusively in response to user actions: + // - either when the `` prop is updated in the controlled ReactFlow setup, + // - 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[]) => { const { defaultEdgeOptions = {} } = get(); @@ -69,7 +74,7 @@ const createRFStore = ({ }; if (hasDefaultNodes) { - nextState.nodes = updateNodes(nodes, new Map(), { + nextState.nodes = adoptUserProvidedNodes(nodes, new Map(), { nodeOrigin: get().nodeOrigin, elevateNodesOnSelect: get().elevateNodesOnSelect, }); @@ -163,7 +168,7 @@ const createRFStore = ({ if (changes?.length) { if (hasDefaultNodes) { const updatedNodes = applyNodeChanges(changes, nodes); - const nextNodes = updateNodes(updatedNodes, nodeLookup, { + const nextNodes = adoptUserProvidedNodes(updatedNodes, nodeLookup, { nodeOrigin, elevateNodesOnSelect, }); diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts index 48e5434f..d54ff61c 100644 --- a/packages/react/src/store/initialState.ts +++ b/packages/react/src/store/initialState.ts @@ -1,7 +1,7 @@ import { infiniteExtent, ConnectionMode, - updateNodes, + adoptUserProvidedNodes, getNodesBounds, getViewportForBounds, Transform, @@ -23,7 +23,7 @@ const getInitialState = ({ fitView?: boolean; } = {}): ReactFlowStore => { const nodeLookup = new Map(); - 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]; diff --git a/packages/svelte/src/lib/store/initial-store.ts b/packages/svelte/src/lib/store/initial-store.ts index dbbb17fc..a3ac2f1e 100644 --- a/packages/svelte/src/lib/store/initial-store.ts +++ b/packages/svelte/src/lib/store/initial-store.ts @@ -15,7 +15,7 @@ import { type OnError, devWarn, type Viewport, - updateNodes, + adoptUserProvidedNodes, getNodesBounds, getViewportForBounds } from '@xyflow/system'; @@ -68,7 +68,7 @@ export const getInitialStore = ({ fitView?: boolean; }) => { const nodeLookup = new Map(); - const nextNodes = updateNodes(nodes, nodeLookup, { + const nextNodes = adoptUserProvidedNodes(nodes, nodeLookup, { nodeOrigin: [0, 0], elevateNodesOnSelect: false }); diff --git a/packages/svelte/src/lib/store/utils.ts b/packages/svelte/src/lib/store/utils.ts index a11bf1f6..2d53ac90 100644 --- a/packages/svelte/src/lib/store/utils.ts +++ b/packages/svelte/src/lib/store/utils.ts @@ -6,7 +6,7 @@ import { type Writable, get } 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'; @@ -133,7 +133,7 @@ export const createNodesStore = ( let elevateNodesOnSelect = true; const _set = (nds: Node[]): Node[] => { - const nextNodes = updateNodes(nds, nodeLookup, { + const nextNodes = adoptUserProvidedNodes(nds, nodeLookup, { elevateNodesOnSelect, defaults }); diff --git a/packages/system/src/types/nodes.ts b/packages/system/src/types/nodes.ts index b55f3f54..33884bf7 100644 --- a/packages/system/src/types/nodes.ts +++ b/packages/system/src/types/nodes.ts @@ -40,6 +40,10 @@ export type NodeBase z?: number; handleBounds?: NodeHandleBounds; 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; }; }; diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts index eca6b8de..34d3d529 100644 --- a/packages/system/src/utils/store.ts +++ b/packages/system/src/utils/store.ts @@ -62,7 +62,7 @@ type UpdateNodesOptions = { defaults?: Partial; }; -export function updateNodes( +export function adoptUserProvidedNodes( nodes: NodeType[], nodeLookup: Map, options: UpdateNodesOptions = { @@ -76,6 +76,8 @@ export function updateNodes( const nextNodes = nodes.map((n) => { const currentStoreNode = nodeLookup.get(n.id); + if (n === currentStoreNode?.[internalsSymbol]?.userProvidedNode.deref()) return currentStoreNode; + const node: NodeType = { ...options.defaults, ...n, @@ -97,6 +99,7 @@ export function updateNodes( value: { handleBounds: currInternals?.handleBounds, z, + userProvidedNode: new WeakRef(n), }, });