diff --git a/examples/react/src/App/routes.ts b/examples/react/src/App/routes.ts
index 2f3d55ba..76ab08e4 100644
--- a/examples/react/src/App/routes.ts
+++ b/examples/react/src/App/routes.ts
@@ -21,6 +21,7 @@ import Interaction from '../examples/Interaction';
import Intersection from '../examples/Intersection';
import Layouting from '../examples/Layouting';
import MultiFlows from '../examples/MultiFlows';
+import MultiSetNodes from '../examples/MultiSetNodes';
import NodeResizer from '../examples/NodeResizer';
import NodeTypeChange from '../examples/NodeTypeChange';
import NodeTypesObjectChange from '../examples/NodeTypesObjectChange';
@@ -180,6 +181,11 @@ const routes: IRoute[] = [
path: 'layouting',
component: Layouting,
},
+ {
+ name: 'Multi setNodes',
+ path: 'multi-setnodes',
+ component: MultiSetNodes,
+ },
{
name: 'Multi Flows',
path: 'multiflows',
diff --git a/examples/react/src/examples/MultiSetNodes/index.tsx b/examples/react/src/examples/MultiSetNodes/index.tsx
index f05c2b50..f04e0ddf 100644
--- a/examples/react/src/examples/MultiSetNodes/index.tsx
+++ b/examples/react/src/examples/MultiSetNodes/index.tsx
@@ -14,52 +14,45 @@ import {
ReactFlowProvider,
} from '@xyflow/react';
-const initNodes: Node[] = [
- {
- id: '1',
+import './style.css';
+
+const initNodes: Node[] = [];
+
+for (let i = 0; i < 100; i++) {
+ initNodes.push({
+ id: i.toString(),
data: {
- label: 'hallo',
+ label: `node ${i + 1}`,
},
- position: { x: 0, y: 0 },
- },
- {
- id: '2',
- data: {
- label: 'world',
- },
- position: { x: 200, y: 0 },
- },
-];
+ position: { x: (i % 10) * 60, y: Math.floor(i / 10) * 60 },
+ });
+}
const initEdges: Edge[] = [];
const CustomNodeFlow = () => {
- const { setNodes } = useReactFlow();
+ const { setNodes, updateNodeData } = useReactFlow();
const [nodes, , onNodesChange] = useNodesState(initNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges);
const onConnect = useCallback((connection: Connection) => setEdges((eds) => addEdge(connection, eds)), [setEdges]);
- const updateNodes = () => {
- setNodes((nds) =>
- nds.map((n) => {
- if (n.id === '1') {
- return { ...n, data: { label: 'updated' } };
- }
+ const multiSetNodes = () => {
+ nodes.forEach((node) =>
+ setNodes((nds) =>
+ nds.map((n) => {
+ if (n.id === node.id) {
+ return { ...n, data: { label: 'node set' } };
+ }
- return n;
- })
+ return n;
+ })
+ )
);
+ };
- setNodes((nds) =>
- nds.map((n) => {
- if (n.id === '2') {
- return { ...n, data: { label: 'updated' } };
- }
-
- return n;
- })
- );
+ const multiUpdateNodes = () => {
+ nodes.forEach((node) => updateNodeData(node.id, { label: 'node update' }));
};
return (
@@ -74,7 +67,8 @@ const CustomNodeFlow = () => {
-
+
+
);
diff --git a/examples/react/src/examples/MultiSetNodes/style.css b/examples/react/src/examples/MultiSetNodes/style.css
new file mode 100644
index 00000000..11f62226
--- /dev/null
+++ b/examples/react/src/examples/MultiSetNodes/style.css
@@ -0,0 +1,3 @@
+.react-flow .react-flow__node {
+ width: 50px;
+}
diff --git a/packages/react/src/hooks/useReactFlow.ts b/packages/react/src/hooks/useReactFlow.ts
index 198599d5..f933664e 100644
--- a/packages/react/src/hooks/useReactFlow.ts
+++ b/packages/react/src/hooks/useReactFlow.ts
@@ -1,4 +1,4 @@
-import { useCallback, useMemo } from 'react';
+import { useCallback, useMemo, useRef } from 'react';
import { getElementsToRemove, getOverlappingArea, isRectObject, nodeToRect, type Rect } from '@xyflow/system';
import useViewportHelper from './useViewportHelper';
@@ -30,6 +30,10 @@ export function useReactFlow();
+ const setNodesTimeout = useRef>();
+
const getNodes = useCallback>(() => {
return store.getState().nodes.map((n) => ({ ...n })) as NodeType[];
}, []);
@@ -50,16 +54,30 @@ export function useReactFlow>((payload) => {
const { nodes, setNodes, hasDefaultNodes, onNodesChange } = store.getState();
- const nextNodes = typeof payload === 'function' ? payload(nodes as NodeType[]) : payload;
+ setNodesData.current = setNodesData.current || nodes;
+ const nextNodes = typeof payload === 'function' ? payload(setNodesData.current as NodeType[]) : payload;
+
+ setNodesData.current = nextNodes;
if (hasDefaultNodes) {
setNodes(nextNodes);
} else if (onNodesChange) {
- const changes =
- nextNodes.length === 0
- ? nodes.map((node) => ({ type: 'remove', id: node.id } as NodeRemoveChange))
- : nextNodes.map((node) => ({ item: node, type: 'reset' } as NodeResetChange));
- onNodesChange(changes);
+ 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));
+ onNodesChange(changes);
+
+ setNodesData.current = undefined;
+ }, 0);
}
}, []);