refactor(react/setNodes): naive batching
This commit is contained in:
@@ -21,6 +21,7 @@ import Interaction from '../examples/Interaction';
|
|||||||
import Intersection from '../examples/Intersection';
|
import Intersection from '../examples/Intersection';
|
||||||
import Layouting from '../examples/Layouting';
|
import Layouting from '../examples/Layouting';
|
||||||
import MultiFlows from '../examples/MultiFlows';
|
import MultiFlows from '../examples/MultiFlows';
|
||||||
|
import MultiSetNodes from '../examples/MultiSetNodes';
|
||||||
import NodeResizer from '../examples/NodeResizer';
|
import NodeResizer from '../examples/NodeResizer';
|
||||||
import NodeTypeChange from '../examples/NodeTypeChange';
|
import NodeTypeChange from '../examples/NodeTypeChange';
|
||||||
import NodeTypesObjectChange from '../examples/NodeTypesObjectChange';
|
import NodeTypesObjectChange from '../examples/NodeTypesObjectChange';
|
||||||
@@ -180,6 +181,11 @@ const routes: IRoute[] = [
|
|||||||
path: 'layouting',
|
path: 'layouting',
|
||||||
component: Layouting,
|
component: Layouting,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Multi setNodes',
|
||||||
|
path: 'multi-setnodes',
|
||||||
|
component: MultiSetNodes,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Multi Flows',
|
name: 'Multi Flows',
|
||||||
path: 'multiflows',
|
path: 'multiflows',
|
||||||
|
|||||||
@@ -14,52 +14,45 @@ import {
|
|||||||
ReactFlowProvider,
|
ReactFlowProvider,
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
|
|
||||||
const initNodes: Node[] = [
|
import './style.css';
|
||||||
{
|
|
||||||
id: '1',
|
const initNodes: Node[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
initNodes.push({
|
||||||
|
id: i.toString(),
|
||||||
data: {
|
data: {
|
||||||
label: 'hallo',
|
label: `node ${i + 1}`,
|
||||||
},
|
},
|
||||||
position: { x: 0, y: 0 },
|
position: { x: (i % 10) * 60, y: Math.floor(i / 10) * 60 },
|
||||||
},
|
});
|
||||||
{
|
}
|
||||||
id: '2',
|
|
||||||
data: {
|
|
||||||
label: 'world',
|
|
||||||
},
|
|
||||||
position: { x: 200, y: 0 },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const initEdges: Edge[] = [];
|
const initEdges: Edge[] = [];
|
||||||
|
|
||||||
const CustomNodeFlow = () => {
|
const CustomNodeFlow = () => {
|
||||||
const { setNodes } = useReactFlow();
|
const { setNodes, updateNodeData } = useReactFlow();
|
||||||
const [nodes, , onNodesChange] = useNodesState(initNodes);
|
const [nodes, , onNodesChange] = useNodesState(initNodes);
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges);
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges);
|
||||||
|
|
||||||
const onConnect = useCallback((connection: Connection) => setEdges((eds) => addEdge(connection, eds)), [setEdges]);
|
const onConnect = useCallback((connection: Connection) => setEdges((eds) => addEdge(connection, eds)), [setEdges]);
|
||||||
|
|
||||||
const updateNodes = () => {
|
const multiSetNodes = () => {
|
||||||
setNodes((nds) =>
|
nodes.forEach((node) =>
|
||||||
nds.map((n) => {
|
setNodes((nds) =>
|
||||||
if (n.id === '1') {
|
nds.map((n) => {
|
||||||
return { ...n, data: { label: 'updated' } };
|
if (n.id === node.id) {
|
||||||
}
|
return { ...n, data: { label: 'node set' } };
|
||||||
|
}
|
||||||
|
|
||||||
return n;
|
return n;
|
||||||
})
|
})
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
setNodes((nds) =>
|
const multiUpdateNodes = () => {
|
||||||
nds.map((n) => {
|
nodes.forEach((node) => updateNodeData(node.id, { label: 'node update' }));
|
||||||
if (n.id === '2') {
|
|
||||||
return { ...n, data: { label: 'updated' } };
|
|
||||||
}
|
|
||||||
|
|
||||||
return n;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -74,7 +67,8 @@ const CustomNodeFlow = () => {
|
|||||||
<Controls />
|
<Controls />
|
||||||
<Background />
|
<Background />
|
||||||
<Panel>
|
<Panel>
|
||||||
<button onClick={updateNodes}>update nodes</button>
|
<button onClick={multiSetNodes}>set nodes</button>
|
||||||
|
<button onClick={multiUpdateNodes}>update nodes</button>
|
||||||
</Panel>
|
</Panel>
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.react-flow .react-flow__node {
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
@@ -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 { getElementsToRemove, getOverlappingArea, isRectObject, nodeToRect, type Rect } from '@xyflow/system';
|
||||||
|
|
||||||
import useViewportHelper from './useViewportHelper';
|
import useViewportHelper from './useViewportHelper';
|
||||||
@@ -30,6 +30,10 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
|
|||||||
const viewportHelper = useViewportHelper();
|
const viewportHelper = useViewportHelper();
|
||||||
const store = useStoreApi();
|
const store = useStoreApi();
|
||||||
|
|
||||||
|
// this is used to handle multiple syncronous setNodes calls
|
||||||
|
const setNodesData = useRef<Node[]>();
|
||||||
|
const setNodesTimeout = useRef<ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
const getNodes = useCallback<Instance.GetNodes<NodeType>>(() => {
|
const getNodes = useCallback<Instance.GetNodes<NodeType>>(() => {
|
||||||
return store.getState().nodes.map((n) => ({ ...n })) as NodeType[];
|
return store.getState().nodes.map((n) => ({ ...n })) as NodeType[];
|
||||||
}, []);
|
}, []);
|
||||||
@@ -50,16 +54,30 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
|
|||||||
|
|
||||||
const setNodes = useCallback<Instance.SetNodes<NodeType>>((payload) => {
|
const setNodes = useCallback<Instance.SetNodes<NodeType>>((payload) => {
|
||||||
const { nodes, setNodes, hasDefaultNodes, onNodesChange } = store.getState();
|
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) {
|
if (hasDefaultNodes) {
|
||||||
setNodes(nextNodes);
|
setNodes(nextNodes);
|
||||||
} else if (onNodesChange) {
|
} else if (onNodesChange) {
|
||||||
const changes =
|
if (setNodesTimeout.current) {
|
||||||
nextNodes.length === 0
|
clearTimeout(setNodesTimeout.current);
|
||||||
? nodes.map((node) => ({ type: 'remove', id: node.id } as NodeRemoveChange))
|
}
|
||||||
: nextNodes.map((node) => ({ item: node, type: 'reset' } as NodeResetChange<NodeType>));
|
|
||||||
onNodesChange(changes);
|
// 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<NodeType>));
|
||||||
|
onNodesChange(changes);
|
||||||
|
|
||||||
|
setNodesData.current = undefined;
|
||||||
|
}, 0);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user