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

This commit is contained in:
Ivan Akulov
2023-11-23 01:17:55 +01:00
parent 8fe8f73ed3
commit 24390162e0
6 changed files with 26 additions and 14 deletions

View File

@@ -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 `<ReactFlow nodes>` 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,
});

View File

@@ -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<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];

View File

@@ -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<string, Node>();
const nextNodes = updateNodes(nodes, nodeLookup, {
const nextNodes = adoptUserProvidedNodes(nodes, nodeLookup, {
nodeOrigin: [0, 0],
elevateNodesOnSelect: false
});

View File

@@ -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
});

View File

@@ -40,6 +40,10 @@ export type NodeBase<T = any, U extends string | undefined = string | undefined>
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<NodeBase>;
};
};

View File

@@ -62,7 +62,7 @@ type UpdateNodesOptions<NodeType extends NodeBase> = {
defaults?: Partial<NodeType>;
};
export function updateNodes<NodeType extends NodeBase>(
export function adoptUserProvidedNodes<NodeType extends NodeBase>(
nodes: NodeType[],
nodeLookup: Map<string, NodeType>,
options: UpdateNodesOptions<NodeType> = {
@@ -76,6 +76,8 @@ export function updateNodes<NodeType extends NodeBase>(
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<NodeType extends NodeBase>(
value: {
handleBounds: currInternals?.handleBounds,
z,
userProvidedNode: new WeakRef(n),
},
});