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); } }, []);