Merge pull request #3729 from xyflow/refactor/store-updater

Refactor: StoreUpdater
This commit is contained in:
Moritz Klack
2023-12-17 10:50:56 +01:00
committed by GitHub
2 changed files with 96 additions and 184 deletions
@@ -7,7 +7,7 @@ const sourceHandleStyleB: CSSProperties = {
left: 'auto', left: 'auto',
}; };
const CustomNode: FC<NodeProps> = ({ data, positionAbsolute }) => { const CustomNode: FC<NodeProps> = ({ data, positionAbsoluteX, positionAbsoluteY }) => {
return ( return (
<> <>
<Handle type="target" position={Position.Top} /> <Handle type="target" position={Position.Top} />
@@ -18,7 +18,7 @@ const CustomNode: FC<NodeProps> = ({ data, positionAbsolute }) => {
<div> <div>
Position:{' '} Position:{' '}
<strong> <strong>
{positionAbsolute.x.toFixed(2)},{positionAbsolute.y.toFixed(2)} {positionAbsoluteX.toFixed(2)},{positionAbsoluteY.toFixed(2)}
</strong> </strong>
</div> </div>
</div> </div>
@@ -3,67 +3,72 @@
* We distinguish between values we can update directly with `useDirectStoreUpdater` (like `snapGrid`) * We distinguish between values we can update directly with `useDirectStoreUpdater` (like `snapGrid`)
* and values that have a dedicated setter function in the store (like `setNodes`). * and values that have a dedicated setter function in the store (like `setNodes`).
*/ */
import { useEffect } from 'react'; import { useEffect, useRef } from 'react';
import { StoreApi } from 'zustand';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import { devWarn, type CoordinateExtent } from '@xyflow/system'; import { type CoordinateExtent } from '@xyflow/system';
import { useStore, useStoreApi } from '../../hooks/useStore'; import { useStore, useStoreApi } from '../../hooks/useStore';
import type { Node, Edge, ReactFlowState, ReactFlowProps, ReactFlowStore } from '../../types'; import type { Node, Edge, ReactFlowState, ReactFlowProps, FitViewOptions } from '../../types';
type StoreUpdaterProps = Pick< // these fields exist in the global store and we need to keep them up to date
ReactFlowProps, const reactFlowFieldsToTrack = [
| 'nodes' 'nodes',
| 'edges' 'edges',
| 'defaultNodes' 'defaultNodes',
| 'defaultEdges' 'defaultEdges',
| 'onConnect' 'onConnect',
| 'onConnectStart' 'onConnectStart',
| 'onConnectEnd' 'onConnectEnd',
| 'onClickConnectStart' 'onClickConnectStart',
| 'onClickConnectEnd' 'onClickConnectEnd',
| 'nodesDraggable' 'nodesDraggable',
| 'nodesConnectable' 'nodesConnectable',
| 'nodesFocusable' 'nodesFocusable',
| 'edgesFocusable' 'edgesFocusable',
| 'edgesUpdatable' 'edgesUpdatable',
| 'minZoom' 'elevateNodesOnSelect',
| 'maxZoom' 'minZoom',
| 'nodeExtent' 'maxZoom',
| 'onNodesChange' 'nodeExtent',
| 'onEdgesChange' 'onNodesChange',
| 'elementsSelectable' 'onEdgesChange',
| 'connectionMode' 'elementsSelectable',
| 'snapToGrid' 'connectionMode',
| 'snapGrid' 'snapGrid',
| 'translateExtent' 'snapToGrid',
| 'connectOnClick' 'translateExtent',
| 'defaultEdgeOptions' 'connectOnClick',
| 'fitView' 'defaultEdgeOptions',
| 'fitViewOptions' 'fitView',
| 'onNodesDelete' 'fitViewOptions',
| 'onEdgesDelete' 'onNodesDelete',
| 'onDelete' 'onEdgesDelete',
| 'onNodeDragStart' 'onDelete',
| 'onNodeDrag' 'onNodeDrag',
| 'onNodeDragStop' 'onNodeDragStart',
| 'onSelectionDragStart' 'onNodeDragStop',
| 'onSelectionDrag' 'onSelectionDrag',
| 'onSelectionDragStop' 'onSelectionDragStart',
| 'onMove' 'onSelectionDragStop',
| 'onMoveStart' 'onMoveStart',
| 'onMoveEnd' 'onMove',
| 'noPanClassName' 'onMoveEnd',
| 'nodeOrigin' 'noPanClassName',
| 'elevateNodesOnSelect' 'nodeOrigin',
| 'autoPanOnConnect' 'autoPanOnConnect',
| 'autoPanOnNodeDrag' 'autoPanOnNodeDrag',
| 'onError' 'onError',
| 'connectionRadius' 'connectionRadius',
| 'isValidConnection' 'isValidConnection',
| 'selectNodesOnDrag' 'selectNodesOnDrag',
| 'nodeDragThreshold' 'nodeDragThreshold',
> & { rfId: string }; ] as const;
type ReactFlowFieldsToTrack = (typeof reactFlowFieldsToTrack)[number];
type StoreUpdaterProps = Pick<ReactFlowProps, ReactFlowFieldsToTrack> & { rfId: string };
// rfId doesn't exist in ReactFlowProps, but it's one of the fields we want to update
const fieldsToTrack = [...reactFlowFieldsToTrack, 'rfId'] as const;
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
setNodes: s.setNodes, setNodes: s.setNodes,
@@ -76,80 +81,7 @@ const selector = (s: ReactFlowState) => ({
reset: s.reset, reset: s.reset,
}); });
function useStoreUpdater<T>(value: T | undefined, setStoreAction: (param: T) => void) { const StoreUpdater = (props: StoreUpdaterProps) => {
useEffect(() => {
if (typeof value !== 'undefined') {
setStoreAction(value);
}
}, [value]);
}
// updates with values in store that don't have a dedicated setter function
function useDirectStoreUpdater(
key: keyof ReactFlowStore,
value: unknown,
setState: StoreApi<ReactFlowState>['setState']
) {
useEffect(() => {
if (typeof value !== 'undefined') {
setState({ [key]: value });
}
}, [value]);
}
const StoreUpdater = ({
nodes,
edges,
defaultNodes,
defaultEdges,
onConnect,
onConnectStart,
onConnectEnd,
onClickConnectStart,
onClickConnectEnd,
nodesDraggable,
nodesConnectable,
nodesFocusable,
edgesFocusable,
edgesUpdatable,
elevateNodesOnSelect,
minZoom,
maxZoom,
nodeExtent,
onNodesChange,
onEdgesChange,
elementsSelectable,
connectionMode,
snapGrid,
snapToGrid,
translateExtent,
connectOnClick,
defaultEdgeOptions,
fitView,
fitViewOptions,
onNodesDelete,
onEdgesDelete,
onDelete,
onNodeDrag,
onNodeDragStart,
onNodeDragStop,
onSelectionDrag,
onSelectionDragStart,
onSelectionDragStop,
onMoveStart,
onMove,
onMoveEnd,
noPanClassName,
nodeOrigin,
rfId,
autoPanOnConnect,
autoPanOnNodeDrag,
onError,
connectionRadius,
isValidConnection,
selectNodesOnDrag,
nodeDragThreshold,
}: StoreUpdaterProps) => {
const { const {
setNodes, setNodes,
setEdges, setEdges,
@@ -163,64 +95,44 @@ const StoreUpdater = ({
const store = useStoreApi(); const store = useStoreApi();
useEffect(() => { useEffect(() => {
const edgesWithDefaults = defaultEdges?.map((e) => ({ ...e, ...defaultEdgeOptions })); const edgesWithDefaults = props.defaultEdges?.map((e) => ({ ...e, ...props.defaultEdgeOptions }));
setDefaultNodesAndEdges(defaultNodes, edgesWithDefaults); setDefaultNodesAndEdges(props.defaultNodes, edgesWithDefaults);
return () => { return () => {
reset(); reset();
}; };
}, []); }, []);
useDirectStoreUpdater('defaultEdgeOptions', defaultEdgeOptions, store.setState); const previousFields = useRef<Partial<StoreUpdaterProps>>({});
useDirectStoreUpdater('connectionMode', connectionMode, store.setState);
useDirectStoreUpdater('onConnect', onConnect, store.setState);
useDirectStoreUpdater('onConnectStart', onConnectStart, store.setState);
useDirectStoreUpdater('onConnectEnd', onConnectEnd, store.setState);
useDirectStoreUpdater('onClickConnectStart', onClickConnectStart, store.setState);
useDirectStoreUpdater('onClickConnectEnd', onClickConnectEnd, store.setState);
useDirectStoreUpdater('nodesDraggable', nodesDraggable, store.setState);
useDirectStoreUpdater('nodesConnectable', nodesConnectable, store.setState);
useDirectStoreUpdater('nodesFocusable', nodesFocusable, store.setState);
useDirectStoreUpdater('edgesFocusable', edgesFocusable, store.setState);
useDirectStoreUpdater('edgesUpdatable', edgesUpdatable, store.setState);
useDirectStoreUpdater('elementsSelectable', elementsSelectable, store.setState);
useDirectStoreUpdater('elevateNodesOnSelect', elevateNodesOnSelect, store.setState);
useDirectStoreUpdater('snapToGrid', snapToGrid, store.setState);
useDirectStoreUpdater('snapGrid', snapGrid, store.setState);
useDirectStoreUpdater('onNodesChange', onNodesChange, store.setState);
useDirectStoreUpdater('onEdgesChange', onEdgesChange, store.setState);
useDirectStoreUpdater('connectOnClick', connectOnClick, store.setState);
useDirectStoreUpdater('fitViewOnInit', fitView, store.setState);
useDirectStoreUpdater('fitViewOnInitOptions', fitViewOptions, store.setState);
useDirectStoreUpdater('onNodesDelete', onNodesDelete, store.setState);
useDirectStoreUpdater('onEdgesDelete', onEdgesDelete, store.setState);
useDirectStoreUpdater('onDelete', onDelete, store.setState);
useDirectStoreUpdater('onNodeDrag', onNodeDrag, store.setState);
useDirectStoreUpdater('onNodeDragStart', onNodeDragStart, store.setState);
useDirectStoreUpdater('onNodeDragStop', onNodeDragStop, store.setState);
useDirectStoreUpdater('onSelectionDrag', onSelectionDrag, store.setState);
useDirectStoreUpdater('onSelectionDragStart', onSelectionDragStart, store.setState);
useDirectStoreUpdater('onSelectionDragStop', onSelectionDragStop, store.setState);
useDirectStoreUpdater('onMove', onMove, store.setState);
useDirectStoreUpdater('onMoveStart', onMoveStart, store.setState);
useDirectStoreUpdater('onMoveEnd', onMoveEnd, store.setState);
useDirectStoreUpdater('noPanClassName', noPanClassName, store.setState);
useDirectStoreUpdater('nodeOrigin', nodeOrigin, store.setState);
useDirectStoreUpdater('rfId', rfId, store.setState);
useDirectStoreUpdater('autoPanOnConnect', autoPanOnConnect, store.setState);
useDirectStoreUpdater('autoPanOnNodeDrag', autoPanOnNodeDrag, store.setState);
useDirectStoreUpdater('onError', onError || devWarn, store.setState);
useDirectStoreUpdater('connectionRadius', connectionRadius, store.setState);
useDirectStoreUpdater('isValidConnection', isValidConnection, store.setState);
useDirectStoreUpdater('selectNodesOnDrag', selectNodesOnDrag, store.setState);
useDirectStoreUpdater('nodeDragThreshold', nodeDragThreshold, store.setState);
useStoreUpdater<Node[]>(nodes, setNodes); useEffect(
useStoreUpdater<Edge[]>(edges, setEdges); () => {
useStoreUpdater<number>(minZoom, setMinZoom); for (const fieldName of fieldsToTrack) {
useStoreUpdater<number>(maxZoom, setMaxZoom); const fieldValue = props[fieldName];
useStoreUpdater<CoordinateExtent>(translateExtent, setTranslateExtent); const previousFieldValue = previousFields.current[fieldName];
useStoreUpdater<CoordinateExtent>(nodeExtent, setNodeExtent);
if (fieldValue === previousFieldValue) continue;
if (typeof props[fieldName] === 'undefined') continue;
// Custom handling with dedicated setters for some fields
if (fieldName === 'nodes') setNodes(fieldValue as Node[]);
else if (fieldName === 'edges') setEdges(fieldValue as Edge[]);
else if (fieldName === 'minZoom') setMinZoom(fieldValue as number);
else if (fieldName === 'maxZoom') setMaxZoom(fieldValue as number);
else if (fieldName === 'translateExtent') setTranslateExtent(fieldValue as CoordinateExtent);
else if (fieldName === 'nodeExtent') setNodeExtent(fieldValue as CoordinateExtent);
// Renamed fields
else if (fieldName === 'fitView') store.setState({ fitViewOnInit: fieldValue as boolean });
else if (fieldName === 'fitViewOptions') store.setState({ fitViewOnInitOptions: fieldValue as FitViewOptions });
// General case
else store.setState({ [fieldName]: fieldValue });
}
previousFields.current = props;
},
// Only re-run the effect if one of the fields we track changes
fieldsToTrack.map((fieldName) => props[fieldName])
);
return null; return null;
}; };