diff --git a/packages/react/src/hooks/useReactFlow.ts b/packages/react/src/hooks/useReactFlow.ts index bdd05aa0..097c78b6 100644 --- a/packages/react/src/hooks/useReactFlow.ts +++ b/packages/react/src/hooks/useReactFlow.ts @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useRef } from 'react'; +import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { getElementsToRemove, getOverlappingArea, isRectObject, nodeToRect, type Rect } from '@xyflow/system'; import useViewportHelper from './useViewportHelper'; @@ -46,33 +46,73 @@ export function useReactFlow e.id === id) as EdgeType; }, []); - // this is used to handle multiple syncronous setNodes calls - const setNodesData = useRef(); - const setNodesTimeout = useRef>(); + const setNodesQueue = useRef<(NodeType[] | ((nodes: NodeType[]) => NodeType[]))[]>([]); + const setNodesHasStateReplacement = useRef(false); + const [setNodesShouldFlushNodesQueue, setSetNodesShouldFlushNodesQueue] = useState(false); const setNodes = useCallback>((payload) => { - const { nodes = [], setNodes, hasDefaultNodes, onNodesChange, nodeLookup } = store.getState(); - const nextNodes = typeof payload === 'function' ? payload((setNodesData.current as NodeType[]) || nodes) : payload; + if (typeof payload === 'function' && !setNodesHasStateReplacement.current) { + setNodesQueue.current.push(payload); + setSetNodesShouldFlushNodesQueue(true); + } else { + setNodesQueue.current = [payload]; + setNodesHasStateReplacement.current = true; + setSetNodesShouldFlushNodesQueue(true); + } + }, []); - setNodesData.current = nextNodes; + useLayoutEffect(() => { + if (!setNodesShouldFlushNodesQueue) { + setNodesQueue.current = []; + setNodesHasStateReplacement.current = false; - if (setNodesTimeout.current) { - clearTimeout(setNodesTimeout.current); + return; } - // 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(() => { - if (hasDefaultNodes) { - setNodes(nextNodes); - } else if (onNodesChange) { - const changes: NodeChange[] = getElementsDiffChanges({ items: setNodesData.current, lookup: nodeLookup }); - onNodesChange(changes); - } + const { nodes = [], setNodes, hasDefaultNodes, onNodesChange, nodeLookup } = store.getState(); + const nextNodes = setNodesQueue.current.reduce( + (prev: NodeType[], payload) => (typeof payload === 'function' ? payload(prev) : payload), + nodes as NodeType[] + ); - setNodesData.current = undefined; - }, 0); - }, []); + if (hasDefaultNodes) { + setNodes(nextNodes); + } else if (onNodesChange) { + const changes: NodeChange[] = getElementsDiffChanges({ items: nextNodes, lookup: nodeLookup }); + onNodesChange(changes); + } + + setNodesQueue.current = []; + setNodesHasStateReplacement.current = false; + setSetNodesShouldFlushNodesQueue(false); + }, [setNodesShouldFlushNodesQueue]); + + // // this is used to handle multiple syncronous setNodes calls + // const setNodesData = useRef(); + // const setNodesTimeout = useRef>(); + // const setNodes = useCallback>((payload) => { + // const { nodes = [], setNodes, hasDefaultNodes, onNodesChange, nodeLookup } = store.getState(); + // const nextNodes = typeof payload === 'function' ? payload((setNodesData.current as NodeType[]) || nodes) : payload; + + // setNodesData.current = nextNodes; + + // 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(() => { + // if (hasDefaultNodes) { + // setNodes(nextNodes); + // } else if (onNodesChange) { + // const changes: NodeChange[] = getElementsDiffChanges({ items: setNodesData.current, lookup: nodeLookup }); + // onNodesChange(changes); + // } + + // setNodesData.current = undefined; + // }, 0); + // }, []); // this is used to handle multiple syncronous setEdges calls const setEdgesData = useRef();