From ee0d29029ae81505e047b9dcd7f2f8030421651c Mon Sep 17 00:00:00 2001 From: moklick Date: Wed, 13 Oct 2021 12:54:01 +0200 Subject: [PATCH] refactor(state): replace redux with zustand --- package-lock.json | 24 +- package.json | 3 +- .../Background/index.tsx | 8 +- src/additional-components/Controls/index.tsx | 11 +- src/additional-components/MiniMap/index.tsx | 12 +- .../ReactFlowProvider/index.tsx | 14 +- src/components/ConnectionLine/index.tsx | 7 +- src/components/Edges/BezierEdge.tsx | 1 - src/components/Edges/wrapEdge.tsx | 38 +- src/components/ElementUpdater/index.tsx | 12 +- src/components/Handle/handler.ts | 8 +- src/components/Handle/index.tsx | 35 +- src/components/Nodes/DefaultNode.tsx | 16 +- src/components/Nodes/utils.ts | 30 +- src/components/Nodes/wrapNode.tsx | 21 +- src/components/NodesSelection/index.tsx | 35 +- src/components/SelectionListener/index.tsx | 10 +- src/components/UserSelection/index.tsx | 31 +- src/container/EdgeRenderer/index.tsx | 41 +- src/container/FlowRenderer/index.tsx | 14 +- src/container/GraphView/index.tsx | 42 +- src/container/NodeRenderer/index.tsx | 30 +- src/container/ReactFlow/Wrapper.tsx | 31 +- src/container/ZoomPane/index.tsx | 24 +- src/hooks/useGlobalKeyHandler.ts | 22 +- src/hooks/useResizeHandler.ts | 7 +- src/hooks/useUpdateNodeInternals.ts | 10 +- src/hooks/useZoomPanHelper.ts | 14 +- src/index.ts | 2 +- src/store/actions.ts | 176 ------- src/store/configure-store.ts | 11 - src/store/contants.ts | 36 -- src/store/hooks.ts | 48 -- src/store/index.ts | 452 +++++++++++++++--- src/store/reducer.ts | 350 -------------- src/store/utils.ts | 5 - src/types/index.ts | 35 ++ src/utils/graph.ts | 16 +- 38 files changed, 786 insertions(+), 896 deletions(-) delete mode 100644 src/store/actions.ts delete mode 100644 src/store/configure-store.ts delete mode 100644 src/store/contants.ts delete mode 100644 src/store/hooks.ts delete mode 100644 src/store/reducer.ts delete mode 100644 src/store/utils.ts diff --git a/package-lock.json b/package-lock.json index 9121c531..a91d8284 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,8 @@ "react-draggable": "^4.4.4", "react-redux": "^7.2.5", "redux": "^4.1.1", - "redux-thunk": "^2.3.0" + "redux-thunk": "^2.3.0", + "zustand": "^3.5.13" }, "devDependencies": { "@babel/core": "^7.15.5", @@ -11257,6 +11258,22 @@ "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } + }, + "node_modules/zustand": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.5.13.tgz", + "integrity": "sha512-orO/XcYwSWffsrPVTdCtuKM/zkUaOIyKDasOk/lecsD3R0euELsj+cB65uKZ1KyinrK2STHIuUhRoLpH8QprQg==", + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } } }, "dependencies": { @@ -19593,6 +19610,11 @@ "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } + }, + "zustand": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.5.13.tgz", + "integrity": "sha512-orO/XcYwSWffsrPVTdCtuKM/zkUaOIyKDasOk/lecsD3R0euELsj+cB65uKZ1KyinrK2STHIuUhRoLpH8QprQg==" } } } diff --git a/package.json b/package.json index 6f81fe7b..2f5d6d2c 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "react-draggable": "^4.4.4", "react-redux": "^7.2.5", "redux": "^4.1.1", - "redux-thunk": "^2.3.0" + "redux-thunk": "^2.3.0", + "zustand": "^3.5.13" }, "devDependencies": { "@babel/core": "^7.15.5", diff --git a/src/additional-components/Background/index.tsx b/src/additional-components/Background/index.tsx index 0e99a835..10ecf3e1 100644 --- a/src/additional-components/Background/index.tsx +++ b/src/additional-components/Background/index.tsx @@ -1,8 +1,8 @@ import React, { memo, useMemo, FC, HTMLAttributes } from 'react'; import cc from 'classcat'; -import { useStoreState } from '../../store/hooks'; -import { BackgroundVariant } from '../../types'; +import { useStore } from '../../store'; +import { BackgroundVariant, ReactFlowState } from '../../types'; import { createGridLinesPath, createGridDotsPath } from './utils'; export interface BackgroundProps extends HTMLAttributes { @@ -17,6 +17,8 @@ const defaultColors = { [BackgroundVariant.Lines]: '#eee', }; +const transformSelector = (s: ReactFlowState) => s.transform; + const Background: FC = ({ variant = BackgroundVariant.Dots, gap = 15, @@ -25,7 +27,7 @@ const Background: FC = ({ style, className, }) => { - const [x, y, scale] = useStoreState((s) => s.transform); + const [x, y, scale] = useStore(transformSelector); // when there are multiple flows on a page we need to make sure that every background gets its own pattern. const patternId = useMemo(() => `pattern-${Math.floor(Math.random() * 100000)}`, []); diff --git a/src/additional-components/Controls/index.tsx b/src/additional-components/Controls/index.tsx index 5e4ece7f..850ee781 100644 --- a/src/additional-components/Controls/index.tsx +++ b/src/additional-components/Controls/index.tsx @@ -1,7 +1,7 @@ import React, { memo, useCallback, HTMLAttributes, FC, useEffect, useState } from 'react'; import cc from 'classcat'; -import { useStoreState, useStoreActions } from '../../store/hooks'; +import { useStore } from '../../store'; import PlusIcon from '../../../assets/icons/plus.svg'; import MinusIcon from '../../../assets/icons/minus.svg'; @@ -10,7 +10,7 @@ import LockIcon from '../../../assets/icons/lock.svg'; import UnlockIcon from '../../../assets/icons/unlock.svg'; import useZoomPanHelper from '../../hooks/useZoomPanHelper'; -import { FitViewParams } from '../../types'; +import { FitViewParams, ReactFlowState } from '../../types'; export interface ControlProps extends HTMLAttributes { showZoom?: boolean; @@ -31,6 +31,9 @@ export const ControlButton: FC = ({ children, className, ... ); +const setInteractiveSelector = (s: ReactFlowState) => s.setInteractive; +const isInteractiveSelector = (s: ReactFlowState) => s.nodesDraggable && s.nodesConnectable && s.elementsSelectable; + const Controls: FC = ({ style, showZoom = true, @@ -45,10 +48,10 @@ const Controls: FC = ({ children, }) => { const [isVisible, setIsVisible] = useState(false); - const setInteractive = useStoreActions((actions) => actions.setInteractive); + const setInteractive = useStore(setInteractiveSelector); + const isInteractive = useStore(isInteractiveSelector); const { zoomIn, zoomOut, fitView } = useZoomPanHelper(); - const isInteractive = useStoreState((s) => s.nodesDraggable && s.nodesConnectable && s.elementsSelectable); const mapClasses = cc(['react-flow__controls', className]); const onZoomInHandler = useCallback(() => { diff --git a/src/additional-components/MiniMap/index.tsx b/src/additional-components/MiniMap/index.tsx index 6060b463..1675f6b1 100644 --- a/src/additional-components/MiniMap/index.tsx +++ b/src/additional-components/MiniMap/index.tsx @@ -1,9 +1,9 @@ import React, { memo, HTMLAttributes } from 'react'; import cc from 'classcat'; -import { useStoreState } from '../../store/hooks'; +import { useStore } from '../../store'; import { getRectOfNodes, getBoundsofRects } from '../../utils/graph'; -import { Node, Rect } from '../../types'; +import { Node, Rect, ReactFlowState } from '../../types'; import MiniMapNode from './MiniMapNode'; type StringFunc = (node: Node) => string; @@ -22,6 +22,8 @@ declare const window: any; const defaultWidth = 200; const defaultHeight = 150; +const selector = (s: ReactFlowState) => ({ width: s.width, height: s.height, transform: s.transform, nodes: s.nodes }); + const MiniMap = ({ style, className, @@ -32,10 +34,8 @@ const MiniMap = ({ nodeStrokeWidth = 2, maskColor = 'rgb(240, 242, 243, 0.7)', }: MiniMapProps) => { - const containerWidth = useStoreState((s) => s.width); - const containerHeight = useStoreState((s) => s.height); - const [tX, tY, tScale] = useStoreState((s) => s.transform); - const nodes = useStoreState((s) => s.nodes); + const { width: containerWidth, height: containerHeight, transform, nodes } = useStore(selector); + const [tX, tY, tScale] = transform; const mapClasses = cc(['react-flow__minimap', className]); const elementWidth = (style?.width || defaultWidth)! as number; diff --git a/src/additional-components/ReactFlowProvider/index.tsx b/src/additional-components/ReactFlowProvider/index.tsx index d1abc923..a7044a3f 100644 --- a/src/additional-components/ReactFlowProvider/index.tsx +++ b/src/additional-components/ReactFlowProvider/index.tsx @@ -1,16 +1,8 @@ -import React, { FC, useMemo } from 'react'; -import { Provider } from 'react-redux'; +import React, { FC } from 'react'; -import { initialState } from '../../store'; -import configureStore from '../../store/configure-store'; +import { Provider, createStore } from '../../store'; -const ReactFlowProvider: FC = ({ children }) => { - const store = useMemo(() => { - return configureStore(initialState); - }, []); - - return {children}; -}; +const ReactFlowProvider: FC = ({ children }) => {children}; ReactFlowProvider.displayName = 'ReactFlowProvider'; diff --git a/src/components/ConnectionLine/index.tsx b/src/components/ConnectionLine/index.tsx index 8c8a5ffc..da1ed174 100644 --- a/src/components/ConnectionLine/index.tsx +++ b/src/components/ConnectionLine/index.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState, CSSProperties } from 'react'; -import { useStoreState } from '../../store/hooks'; +import { useStore } from '../../store'; import { getBezierPath } from '../Edges/BezierEdge'; import { getSmoothStepPath } from '../Edges/SmoothStepEdge'; import { @@ -12,6 +12,7 @@ import { ConnectionLineType, ConnectionLineComponent, HandleType, + ReactFlowState, } from '../../types'; interface ConnectionLineProps { @@ -27,6 +28,8 @@ interface ConnectionLineProps { CustomConnectionLineComponent?: ConnectionLineComponent; } +const nodesSelector = (s: ReactFlowState) => s.nodes; + export default ({ connectionNodeId, connectionHandleId, @@ -39,7 +42,7 @@ export default ({ isConnectable, CustomConnectionLineComponent, }: ConnectionLineProps) => { - const nodes = useStoreState((state) => state.nodes); + const nodes = useStore(nodesSelector); const [sourceNode, setSourceNode] = useState(null); const nodeId = connectionNodeId; const handleId = connectionHandleId; diff --git a/src/components/Edges/BezierEdge.tsx b/src/components/Edges/BezierEdge.tsx index 368fdcc6..b75eb949 100644 --- a/src/components/Edges/BezierEdge.tsx +++ b/src/components/Edges/BezierEdge.tsx @@ -1,7 +1,6 @@ import React, { memo } from 'react'; import EdgeText from './EdgeText'; - import { getMarkerEnd, getCenter } from './utils'; import { EdgeProps, Position } from '../../types'; diff --git a/src/components/Edges/wrapEdge.tsx b/src/components/Edges/wrapEdge.tsx index 60384e74..4e85d345 100644 --- a/src/components/Edges/wrapEdge.tsx +++ b/src/components/Edges/wrapEdge.tsx @@ -1,11 +1,19 @@ import React, { memo, ComponentType, useCallback, useState, useMemo } from 'react'; import cc from 'classcat'; -import { useStoreActions, useStoreState } from '../../store/hooks'; -import { Edge, EdgeProps, WrapEdgeProps } from '../../types'; +import { useStore, useStoreApi } from '../../store'; +import { Edge, EdgeProps, WrapEdgeProps, ReactFlowState } from '../../types'; import { onMouseDown } from '../../components/Handle/handler'; import { EdgeAnchor } from './EdgeAnchor'; +const selector = (s: ReactFlowState) => ({ + addSelectedElements: s.addSelectedElements, + setConnectionNodeId: s.setConnectionNodeId, + unsetNodesSelection: s.unsetNodesSelection, + setPosition: s.setConnectionPosition, + connectionMode: s.connectionMode, +}); + export default (EdgeComponent: ComponentType) => { const EdgeWrapper = ({ id, @@ -47,11 +55,9 @@ export default (EdgeComponent: ComponentType) => { onEdgeUpdateStart, onEdgeUpdateEnd, }: WrapEdgeProps): JSX.Element | null => { - const addSelectedElements = useStoreActions((actions) => actions.addSelectedElements); - const setConnectionNodeId = useStoreActions((actions) => actions.setConnectionNodeId); - const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection); - const setPosition = useStoreActions((actions) => actions.setConnectionPosition); - const connectionMode = useStoreState((state) => state.connectionMode); + const store = useStoreApi(); + const { addSelectedElements, setConnectionNodeId, unsetNodesSelection, setPosition, connectionMode } = + useStore(selector); const [updating, setUpdating] = useState(false); @@ -90,7 +96,7 @@ export default (EdgeComponent: ComponentType) => { (event: React.MouseEvent): void => { if (elementsSelectable) { unsetNodesSelection(); - addSelectedElements(edgeElement); + addSelectedElements([edgeElement]); } onClick?.(event, edgeElement); @@ -157,10 +163,22 @@ export default (EdgeComponent: ComponentType) => { isValidConnection, connectionMode, isSourceHandle ? 'target' : 'source', - _onEdgeUpdate + _onEdgeUpdate, + store.getState ); }, - [id, source, target, type, sourceHandleId, targetHandleId, setConnectionNodeId, setPosition, edgeElement, onConnectEdge] + [ + id, + source, + target, + type, + sourceHandleId, + targetHandleId, + setConnectionNodeId, + setPosition, + edgeElement, + onConnectEdge, + ] ); const onEdgeUpdaterSourceMouseDown = useCallback( diff --git a/src/components/ElementUpdater/index.tsx b/src/components/ElementUpdater/index.tsx index f40636da..555abc7a 100644 --- a/src/components/ElementUpdater/index.tsx +++ b/src/components/ElementUpdater/index.tsx @@ -1,16 +1,20 @@ import { useEffect } from 'react'; -import { useStoreActions } from '../../store/hooks'; -import { Node, Edge } from '../../types'; +import { useStore } from '../../store'; +import { Node, Edge, ReactFlowState } from '../../types'; interface ElementUpdaterProps { nodes: Node[]; edges: Edge[]; } +const selector = (s: ReactFlowState) => ({ + setNodes: s.setNodes, + setEdges: s.setEdges, +}); + const ElementUpdater = ({ nodes, edges }: ElementUpdaterProps) => { - const setNodes = useStoreActions((actions) => actions.setNodes); - const setEdges = useStoreActions((actions) => actions.setEdges); + const { setNodes, setEdges } = useStore(selector); useEffect(() => { setNodes(nodes); diff --git a/src/components/Handle/handler.ts b/src/components/Handle/handler.ts index f2733a77..53f7d213 100644 --- a/src/components/Handle/handler.ts +++ b/src/components/Handle/handler.ts @@ -1,9 +1,8 @@ import { MouseEvent as ReactMouseEvent } from 'react'; -import { Store } from 'redux'; +import { GetState } from 'zustand'; import { getHostForElement } from '../../utils'; import { ReactFlowState } from '../../types'; -import { ReactFlowAction } from '../../store/actions'; import { ElementId, @@ -107,7 +106,7 @@ export function onMouseDown( onConnectStart?: OnConnectStartFunc, onConnectStop?: OnConnectStopFunc, onConnectEnd?: OnConnectEndFunc, - store?: Store + getState?: GetState ): void { const reactFlowNode = (event.target as Element).closest('.react-flow'); // when react-flow is used inside a shadow root we can't use document @@ -181,7 +180,8 @@ export function onMouseDown( onConnectStop?.(event); if (isValid) { - onConnect?.(connection, store?.getState().nodes || []); + const nodes = getState?.().nodes; + onConnect?.(connection, nodes || []); } onConnectEnd?.(event); diff --git a/src/components/Handle/index.tsx b/src/components/Handle/index.tsx index 317efe65..e8a152b6 100644 --- a/src/components/Handle/index.tsx +++ b/src/components/Handle/index.tsx @@ -1,9 +1,9 @@ import React, { memo, useContext, useCallback, HTMLAttributes, forwardRef } from 'react'; import cc from 'classcat'; -import { useStoreActions, useStoreState, useStore } from '../../store/hooks'; +import { useStore } from '../../store'; import NodeIdContext from '../../contexts/NodeIdContext'; -import { HandleProps, Connection, ElementId, Position, Node } from '../../types'; +import { HandleProps, Connection, ElementId, Position, Node, ReactFlowState } from '../../types'; import { onMouseDown, SetSourceIdFunc, SetPosition } from './handler'; @@ -11,6 +11,16 @@ const alwaysValid = () => true; export type HandleComponentProps = HandleProps & Omit, 'id'>; +const selector = (s: ReactFlowState) => ({ + setPosition: s.setConnectionPosition, + setConnectionNodeId: s.setConnectionNodeId, + onConnectAction: s.onConnect, + onConnectStart: s.onConnectStart, + onConnectStop: s.onConnectStop, + onConnectEnd: s.onConnectEnd, + connectionMode: s.connectionMode, +}); + const Handle = forwardRef( ( { @@ -26,15 +36,17 @@ const Handle = forwardRef( }, ref ) => { - const store = useStore(); const nodeId = useContext(NodeIdContext) as ElementId; - const setPosition = useStoreActions((actions) => actions.setConnectionPosition); - const setConnectionNodeId = useStoreActions((actions) => actions.setConnectionNodeId); - const onConnectAction = useStoreState((state) => state.onConnect); - const onConnectStart = useStoreState((state) => state.onConnectStart); - const onConnectStop = useStoreState((state) => state.onConnectStop); - const onConnectEnd = useStoreState((state) => state.onConnectEnd); - const connectionMode = useStoreState((state) => state.connectionMode); + const { + setPosition, + setConnectionNodeId, + onConnectAction, + onConnectStart, + onConnectStop, + onConnectEnd, + connectionMode, + } = useStore(selector); + const handleId = id || null; const isTarget = type === 'target'; @@ -62,8 +74,7 @@ const Handle = forwardRef( undefined, onConnectStart, onConnectStop, - onConnectEnd, - store + onConnectEnd ); }, [ diff --git a/src/components/Nodes/DefaultNode.tsx b/src/components/Nodes/DefaultNode.tsx index 11d9af2f..7d9dc5d5 100644 --- a/src/components/Nodes/DefaultNode.tsx +++ b/src/components/Nodes/DefaultNode.tsx @@ -8,13 +8,15 @@ const DefaultNode = ({ isConnectable, targetPosition = Position.Top, sourcePosition = Position.Bottom, -}: NodeProps) => ( - <> - - {data.label} - - -); +}: NodeProps) => { + return ( + <> + + {data.label} + + + ); +}; DefaultNode.displayName = 'DefaultNode'; diff --git a/src/components/Nodes/utils.ts b/src/components/Nodes/utils.ts index e917e853..1ed82013 100644 --- a/src/components/Nodes/utils.ts +++ b/src/components/Nodes/utils.ts @@ -13,7 +13,7 @@ export const getHandleBounds = (nodeElement: HTMLDivElement, scale: number) => { export const getHandleBoundsByHandleType = ( selector: string, nodeElement: HTMLDivElement, - parentBounds: ClientRect | DOMRect, + parentBounds: DOMRect, k: number ): HandleElement[] | null => { const handles = nodeElement.querySelectorAll(selector); @@ -24,20 +24,18 @@ export const getHandleBoundsByHandleType = ( const handlesArray = Array.from(handles) as HTMLDivElement[]; - return handlesArray.map( - (handle): HandleElement => { - const bounds = handle.getBoundingClientRect(); - const dimensions = getDimensions(handle); - const handleId = handle.getAttribute('data-handleid'); - const handlePosition = (handle.getAttribute('data-handlepos') as unknown) as Position; + return handlesArray.map((handle): HandleElement => { + const bounds = handle.getBoundingClientRect(); + const dimensions = getDimensions(handle); + const handleId = handle.getAttribute('data-handleid'); + const handlePosition = handle.getAttribute('data-handlepos') as unknown as Position; - return { - id: handleId, - position: handlePosition, - x: (bounds.left - parentBounds.left) / k, - y: (bounds.top - parentBounds.top) / k, - ...dimensions, - }; - } - ); + return { + id: handleId, + position: handlePosition, + x: (bounds.left - parentBounds.left) / k, + y: (bounds.top - parentBounds.top) / k, + ...dimensions, + }; + }); }; diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index 04f9acef..285a011e 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -2,9 +2,15 @@ import React, { useEffect, useRef, memo, ComponentType, CSSProperties, useMemo, import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable'; import cc from 'classcat'; -import { useStoreActions, useStoreState } from '../../store/hooks'; +import { useStore } from '../../store'; import { Provider } from '../../contexts/NodeIdContext'; -import { NodeComponentProps, WrapNodeProps } from '../../types'; +import { NodeComponentProps, WrapNodeProps, ReactFlowState } from '../../types'; + +const selector = (s: ReactFlowState) => ({ + addSelectedElements: s.addSelectedElements, + onNodesChange: s.onNodesChange, + unsetNodesSelection: s.unsetNodesSelection, +}); export default (NodeComponent: ComponentType) => { const NodeWrapper = ({ @@ -41,10 +47,7 @@ export default (NodeComponent: ComponentType) => { dragHandle, }: WrapNodeProps) => { // const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions); - const addSelectedElements = useStoreActions((actions) => actions.addSelectedElements); - const onNodesChange = useStoreState((state) => state.onNodesChange); - const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection); - + const { addSelectedElements, onNodesChange, unsetNodesSelection } = useStore(selector); const nodeElement = useRef(null); const node = useMemo(() => ({ id, type, position: { x: xPos, y: yPos }, data }), [id, type, xPos, yPos, data]); @@ -114,7 +117,7 @@ export default (NodeComponent: ComponentType) => { unsetNodesSelection(); if (!selected) { - addSelectedElements(node); + addSelectedElements([node]); } } @@ -132,7 +135,7 @@ export default (NodeComponent: ComponentType) => { unsetNodesSelection(); if (!selected) { - addSelectedElements(node); + addSelectedElements([node]); } } else if (!selectNodesOnDrag && !selected && isSelectable) { unsetNodesSelection(); @@ -173,7 +176,7 @@ export default (NodeComponent: ComponentType) => { // Because of that we set dragging to true inside the onDrag handler and handle the click here if (!isDragging) { if (isSelectable && !selectNodesOnDrag && !selected) { - addSelectedElements(node); + addSelectedElements([node]); } onClick?.(event as MouseEvent, node); diff --git a/src/components/NodesSelection/index.tsx b/src/components/NodesSelection/index.tsx index 1b3c3257..26a792f0 100644 --- a/src/components/NodesSelection/index.tsx +++ b/src/components/NodesSelection/index.tsx @@ -6,9 +6,9 @@ import React, { useMemo, useCallback, useRef, MouseEvent } from 'react'; import ReactDraggable, { DraggableData } from 'react-draggable'; -import { useStoreState, useStoreActions } from '../../store/hooks'; +import { useStore } from '../../store'; import { isNode } from '../../utils/graph'; -import { Node } from '../../types'; +import { Node, ReactFlowState } from '../../types'; export interface NodesSelectionProps { onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void; @@ -17,21 +17,34 @@ export interface NodesSelectionProps { onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void; } +const selector = (s: ReactFlowState) => ({ + transform: s.transform, + selectedNodesBbox: s.selectedNodesBbox, + selectionActive: s.selectionActive, + selectedElements: s.selectedElements, + snapToGrid: s.snapToGrid, + snapGrid: s.snapGrid, + nodes: s.nodes, + updateNodePosDiff: s.updateNodePosDiff, +}); + export default ({ onSelectionDragStart, onSelectionDrag, onSelectionDragStop, onSelectionContextMenu, }: NodesSelectionProps) => { - const [tX, tY, tScale] = useStoreState((state) => state.transform); - const selectedNodesBbox = useStoreState((state) => state.selectedNodesBbox); - const selectionActive = useStoreState((state) => state.selectionActive); - const selectedElements = useStoreState((state) => state.selectedElements); - const snapToGrid = useStoreState((state) => state.snapToGrid); - const snapGrid = useStoreState((state) => state.snapGrid); - const nodes = useStoreState((state) => state.nodes); - - const updateNodePosDiff = useStoreActions((actions) => actions.updateNodePosDiff); + const { + transform, + selectedNodesBbox, + selectionActive, + selectedElements, + snapToGrid, + snapGrid, + nodes, + updateNodePosDiff, + } = useStore(selector); + const [tX, tY, tScale] = transform; const nodeRef = useRef(null); diff --git a/src/components/SelectionListener/index.tsx b/src/components/SelectionListener/index.tsx index 98e1e0d4..ecda15db 100644 --- a/src/components/SelectionListener/index.tsx +++ b/src/components/SelectionListener/index.tsx @@ -1,18 +1,18 @@ import { useEffect } from 'react'; -import { Elements } from '../../types'; -import { useStoreState } from '../../store/hooks'; +import { Elements, ReactFlowState } from '../../types'; +import { useStore } from '../../store'; interface SelectionListenerProps { onSelectionChange: (elements: Elements | null) => void; } +const selectedElementsSelector = (s: ReactFlowState) => s.selectedElements; + // This is just a helper component for calling the onSelectionChange listener. -// As soon as easy-peasy has implemented the effectOn hook, we can remove this component -// and use the hook instead. https://github.com/ctrlplusb/easy-peasy/pull/459 export default ({ onSelectionChange }: SelectionListenerProps) => { - const selectedElements = useStoreState((s) => s.selectedElements); + const selectedElements = useStore(selectedElementsSelector); useEffect(() => { onSelectionChange(selectedElements); diff --git a/src/components/UserSelection/index.tsx b/src/components/UserSelection/index.tsx index 14a8a13a..ca0fe9d2 100644 --- a/src/components/UserSelection/index.tsx +++ b/src/components/UserSelection/index.tsx @@ -4,8 +4,8 @@ import React, { memo } from 'react'; -import { useStoreActions, useStoreState } from '../../store/hooks'; -import { XYPosition } from '../../types'; +import { useStore } from '../../store'; +import { XYPosition, ReactFlowState } from '../../types'; type UserSelectionProps = { selectionKeyPressed: boolean; @@ -25,8 +25,10 @@ function getMousePosition(event: React.MouseEvent): XYPosition | void { }; } +const userSelectionRectSelector = (state: ReactFlowState) => state.userSelectionRect; + const SelectionRect = () => { - const userSelectionRect = useStoreState((state) => state.userSelectionRect); + const userSelectionRect = useStore(userSelectionRectSelector); if (!userSelectionRect.draw) { return null; @@ -44,14 +46,25 @@ const SelectionRect = () => { ); }; +const selector = (s: ReactFlowState) => ({ + selectionActive: s.selectionActive, + elementsSelectable: s.elementsSelectable, + setUserSelection: s.setUserSelection, + updateUserSelection: s.updateUserSelection, + unsetUserSelection: s.unsetUserSelection, + unsetNodesSelection: s.unsetNodesSelection, +}); + export default memo(({ selectionKeyPressed }: UserSelectionProps) => { - const selectionActive = useStoreState((state) => state.selectionActive); - const elementsSelectable = useStoreState((state) => state.elementsSelectable); + const { + selectionActive, + elementsSelectable, + setUserSelection, + updateUserSelection, + unsetUserSelection, + unsetNodesSelection, + } = useStore(selector); - const setUserSelection = useStoreActions((actions) => actions.setUserSelection); - const updateUserSelection = useStoreActions((actions) => actions.updateUserSelection); - const unsetUserSelection = useStoreActions((actions) => actions.unsetUserSelection); - const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection); const renderUserSelectionPane = selectionActive || selectionKeyPressed; if (!elementsSelectable || !renderUserSelectionPane) { diff --git a/src/container/EdgeRenderer/index.tsx b/src/container/EdgeRenderer/index.tsx index 3554d2f8..6c922d6d 100644 --- a/src/container/EdgeRenderer/index.tsx +++ b/src/container/EdgeRenderer/index.tsx @@ -1,6 +1,6 @@ import React, { memo, CSSProperties, useCallback } from 'react'; -import { useStoreState } from '../../store/hooks'; +import { useStore } from '../../store'; import ConnectionLine from '../../components/ConnectionLine/index'; import { isEdge } from '../../utils/graph'; import MarkerDefinitions from './MarkerDefinitions'; @@ -15,6 +15,7 @@ import { ConnectionLineComponent, ConnectionMode, OnEdgeUpdateFunc, + ReactFlowState, } from '../../types'; interface EdgeRendererProps { @@ -199,18 +200,34 @@ const Edge = memo( } ); +const selector = (s: ReactFlowState) => ({ + transform: s.transform, + edges: s.edges, + connectionNodeId: s.connectionNodeId, + connectionHandleId: s.connectionHandleId, + connectionHandleType: s.connectionHandleType, + connectionPosition: s.connectionPosition, + selectedElements: s.selectedElements, + nodesConnectable: s.nodesConnectable, + elementsSelectable: s.elementsSelectable, + width: s.width, + height: s.height, +}); + const EdgeRenderer = (props: EdgeRendererProps) => { - const transform = useStoreState((state) => state.transform); - const edges = useStoreState((state) => state.edges); - const connectionNodeId = useStoreState((state) => state.connectionNodeId); - const connectionHandleId = useStoreState((state) => state.connectionHandleId); - const connectionHandleType = useStoreState((state) => state.connectionHandleType); - const connectionPosition = useStoreState((state) => state.connectionPosition); - const selectedElements = useStoreState((state) => state.selectedElements); - const nodesConnectable = useStoreState((state) => state.nodesConnectable); - const elementsSelectable = useStoreState((state) => state.elementsSelectable); - const width = useStoreState((state) => state.width); - const height = useStoreState((state) => state.height); + const { + transform, + edges, + connectionNodeId, + connectionHandleId, + connectionHandleType, + connectionPosition, + selectedElements, + nodesConnectable, + elementsSelectable, + width, + height, + } = useStore(selector); if (!width) { return null; diff --git a/src/container/FlowRenderer/index.tsx b/src/container/FlowRenderer/index.tsx index 58ee5884..39d951e5 100644 --- a/src/container/FlowRenderer/index.tsx +++ b/src/container/FlowRenderer/index.tsx @@ -1,13 +1,13 @@ import React, { useCallback, memo, ReactNode, WheelEvent, MouseEvent } from 'react'; -import { useStoreActions, useStoreState } from '../../store/hooks'; +import { useStore } from '../../store'; import useGlobalKeyHandler from '../../hooks/useGlobalKeyHandler'; import useKeyPress from '../../hooks/useKeyPress'; - import { GraphViewProps } from '../GraphView'; import ZoomPane from '../ZoomPane'; import UserSelection from '../../components/UserSelection'; import NodesSelection from '../../components/NodesSelection'; +import { ReactFlowState } from '../../types'; interface FlowRendererProps extends Omit< @@ -25,6 +25,12 @@ interface FlowRendererProps children: ReactNode; } +const selector = (s: ReactFlowState) => ({ + unsetNodesSelection: s.unsetNodesSelection, + resetSelectedElements: s.resetSelectedElements, + nodesSelectionActive: s.nodesSelectionActive, +}); + const FlowRenderer = ({ children, onPaneClick, @@ -54,9 +60,7 @@ const FlowRenderer = ({ onSelectionDragStop, onSelectionContextMenu, }: FlowRendererProps) => { - const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection); - const resetSelectedElements = useStoreActions((actions) => actions.resetSelectedElements); - const nodesSelectionActive = useStoreState((state) => state.nodesSelectionActive); + const { unsetNodesSelection, resetSelectedElements, nodesSelectionActive } = useStore(selector); const selectionKeyPressed = useKeyPress(selectionKeyCode); diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index efcfe4e5..bfcbf045 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef, memo } from 'react'; -import { useStoreActions, useStore } from '../../store/hooks'; +import { useStore, useStoreApi } from '../../store'; import FlowRenderer from '../FlowRenderer'; import NodeRenderer from '../NodeRenderer'; import EdgeRenderer from '../EdgeRenderer'; @@ -98,24 +98,24 @@ const GraphView = ({ onEdgesChange, }: GraphViewProps) => { const isInitialized = useRef(false); - const setOnConnect = useStoreActions((actions) => actions.setOnConnect); - const setOnConnectStart = useStoreActions((actions) => actions.setOnConnectStart); - const setOnConnectStop = useStoreActions((actions) => actions.setOnConnectStop); - const setOnConnectEnd = useStoreActions((actions) => actions.setOnConnectEnd); - const setSnapGrid = useStoreActions((actions) => actions.setSnapGrid); - const setSnapToGrid = useStoreActions((actions) => actions.setSnapToGrid); - const setNodesDraggable = useStoreActions((actions) => actions.setNodesDraggable); - const setNodesConnectable = useStoreActions((actions) => actions.setNodesConnectable); - const setElementsSelectable = useStoreActions((actions) => actions.setElementsSelectable); - const setMinZoom = useStoreActions((actions) => actions.setMinZoom); - const setMaxZoom = useStoreActions((actions) => actions.setMaxZoom); - const setTranslateExtent = useStoreActions((actions) => actions.setTranslateExtent); - const setNodeExtent = useStoreActions((actions) => actions.setNodeExtent); - const setConnectionMode = useStoreActions((actions) => actions.setConnectionMode); - const setOnNodesChange = useStoreActions((actions) => actions.setOnNodesChange); - const setOnEdgesChange = useStoreActions((actions) => actions.setOnEdgesChange); + const store = useStoreApi(); + const setOnConnect = useStore((s) => s.setOnConnect); + const setOnConnectStart = useStore((s) => s.setOnConnectStart); + const setOnConnectStop = useStore((s) => s.setOnConnectStop); + const setOnConnectEnd = useStore((s) => s.setOnConnectEnd); + const setSnapGrid = useStore((s) => s.setSnapGrid); + const setSnapToGrid = useStore((s) => s.setSnapToGrid); + const setNodesDraggable = useStore((s) => s.setNodesDraggable); + const setNodesConnectable = useStore((s) => s.setNodesConnectable); + const setElementsSelectable = useStore((s) => s.setElementsSelectable); + const setMinZoom = useStore((s) => s.setMinZoom); + const setMaxZoom = useStore((s) => s.setMaxZoom); + const setTranslateExtent = useStore((s) => s.setTranslateExtent); + const setNodeExtent = useStore((s) => s.setNodeExtent); + const setConnectionMode = useStore((s) => s.setConnectionMode); + const setOnNodesChange = useStore((s) => s.setOnNodesChange); + const setOnEdgesChange = useStore((s) => s.setOnEdgesChange); - const currentStore = useStore(); const { zoomIn, zoomOut, zoomTo, transform, fitView, initialized } = useZoomPanHelper(); useEffect(() => { @@ -127,9 +127,9 @@ const GraphView = ({ zoomOut, zoomTo, setTransform: transform, - project: onLoadProject(currentStore), - getElements: onLoadGetElements(currentStore), - toObject: onLoadToObject(currentStore), + project: onLoadProject(store.getState), + getElements: onLoadGetElements(store.getState), + toObject: onLoadToObject(store.getState), }); } diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index 97669df3..cc47f8f3 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -1,7 +1,7 @@ import React, { memo, useMemo, ComponentType, MouseEvent } from 'react'; -import { useStoreState, useStoreActions } from '../../store/hooks'; -import { Node, NodeTypesType, WrapNodeProps, Edge } from '../../types'; +import { useStore } from '../../store'; +import { Node, NodeTypesType, ReactFlowState, Edge, WrapNodeProps } from '../../types'; interface NodeRendererProps { nodeTypes: NodeTypesType; selectNodesOnDrag: boolean; @@ -19,14 +19,26 @@ interface NodeRendererProps { onlyRenderVisibleElements: boolean; } +const selector = (s: ReactFlowState) => ({ + transform: s.transform, + selectedElements: s.selectedElements, + nodesDraggable: s.nodesDraggable, + nodesConnectable: s.nodesConnectable, + elementsSelectable: s.elementsSelectable, + nodes: s.nodes, + updateNodeDimensions: s.updateNodeDimensions, +}); + const NodeRenderer = (props: NodeRendererProps) => { - const transform = useStoreState((state) => state.transform); - const selectedElements = useStoreState((state) => state.selectedElements); - const nodesDraggable = useStoreState((state) => state.nodesDraggable); - const nodesConnectable = useStoreState((state) => state.nodesConnectable); - const elementsSelectable = useStoreState((state) => state.elementsSelectable); - const nodes = useStoreState((state) => state.nodes); - const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions); + const { + transform, + selectedElements, + nodesDraggable, + nodesConnectable, + elementsSelectable, + nodes, + updateNodeDimensions, + } = useStore(selector); // const visibleNodes = props.onlyRenderVisibleElements // ? getNodesInside(nodes, { x: 0, y: 0, width, height }, transform, true) diff --git a/src/container/ReactFlow/Wrapper.tsx b/src/container/ReactFlow/Wrapper.tsx index 4a41a47c..67f22cf6 100644 --- a/src/container/ReactFlow/Wrapper.tsx +++ b/src/container/ReactFlow/Wrapper.tsx @@ -1,21 +1,26 @@ -import React, { FC, useContext, useMemo } from 'react'; -import { Provider, ReactReduxContext } from 'react-redux'; +import React, { FC } from 'react'; -import store from '../../store'; +import { Provider, createStore } from '../../store'; +// import { ReactFlowState } from '../../types'; + +// const reactFlowVersionSelector = (s: ReactFlowState) => s.reactFlowVersion; const Wrapper: FC = ({ children }) => { - const contextValue = useContext(ReactReduxContext); - const isWrappedWithReactFlowProvider = useMemo(() => contextValue?.store?.getState()?.reactFlowVersion, [ - contextValue, - ]); + // let isWrapped = useRef(true); - if (isWrappedWithReactFlowProvider) { - // we need to wrap it with a fragment because it's not allowed for children to be a ReactNode - // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18051 - return <>{children}; - } + // try { + // useStoreApi(); + // } catch { + // isWrapped.current = false; + // } - return {children}; + // if (isWrapped) { + // // we need to wrap it with a fragment because it's not allowed for children to be a ReactNode + // // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18051 + // return <>{children}; + // } + + return {children}; }; Wrapper.displayName = 'ReactFlowWrapper'; diff --git a/src/container/ZoomPane/index.tsx b/src/container/ZoomPane/index.tsx index 5af2659b..6f62473a 100644 --- a/src/container/ZoomPane/index.tsx +++ b/src/container/ZoomPane/index.tsx @@ -5,8 +5,8 @@ import { select, pointer } from 'd3-selection'; import { clamp } from '../../utils'; import useKeyPress from '../../hooks/useKeyPress'; import useResizeHandler from '../../hooks/useResizeHandler'; -import { useStoreState, useStoreActions, useStore } from '../../store/hooks'; -import { FlowTransform, TranslateExtent, PanOnScrollMode, KeyCode } from '../../types'; +import { useStore, useStoreApi } from '../../store'; +import { FlowTransform, TranslateExtent, PanOnScrollMode, KeyCode, ReactFlowState } from '../../types'; interface ZoomPaneProps { selectionKeyPressed: boolean; @@ -42,6 +42,15 @@ const eventToFlowTransform = (eventTransform: any): FlowTransform => ({ const hasNoWheelClass = (event: any) => event.target.closest('.nowheel'); +const selector = (s: ReactFlowState) => ({ + d3Zoom: s.d3Zoom, + d3Selection: s.d3Selection, + d3ZoomHandler: s.d3ZoomHandler, + + initD3Zoom: s.initD3Zoom, + updateTransform: s.updateTransform, +}); + const ZoomPane = ({ onMove, onMoveStart, @@ -62,17 +71,10 @@ const ZoomPane = ({ preventScrolling = true, children, }: ZoomPaneProps) => { + const store = useStoreApi(); const zoomPane = useRef(null); const prevTransform = useRef({ x: 0, y: 0, zoom: 0 }); - - const store = useStore(); - const d3Zoom = useStoreState((s) => s.d3Zoom); - const d3Selection = useStoreState((s) => s.d3Selection); - const d3ZoomHandler = useStoreState((s) => s.d3ZoomHandler); - - const initD3Zoom = useStoreActions((actions) => actions.initD3Zoom); - const updateTransform = useStoreActions((actions) => actions.updateTransform); - + const { d3Zoom, d3Selection, d3ZoomHandler, initD3Zoom, updateTransform } = useStore(selector); const zoomActivationKeyPressed = useKeyPress(zoomActivationKeyCode); useResizeHandler(zoomPane); diff --git a/src/hooks/useGlobalKeyHandler.ts b/src/hooks/useGlobalKeyHandler.ts index 4a09041b..dcb9eade 100644 --- a/src/hooks/useGlobalKeyHandler.ts +++ b/src/hooks/useGlobalKeyHandler.ts @@ -1,23 +1,27 @@ import { useEffect } from 'react'; -import { useStore, useStoreActions, useStoreState } from '../store/hooks'; +import { useStore, useStoreApi } from '../store'; import useKeyPress from './useKeyPress'; import { isNode, isEdge, getConnectedEdges } from '../utils/graph'; -import { KeyCode } from '../types'; +import { KeyCode, ReactFlowState } from '../types'; interface HookParams { deleteKeyCode: KeyCode; multiSelectionKeyCode: KeyCode; } -export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => { - const store = useStore(); +const selector = (s: ReactFlowState) => ({ + unsetNodesSelection: s.unsetNodesSelection, + setMultiSelectionActive: s.setMultiSelectionActive, + resetSelectedElements: s.resetSelectedElements, + onNodesChange: s.onNodesChange, + onEdgesChange: s.onEdgesChange, +}); - const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection); - const setMultiSelectionActive = useStoreActions((actions) => actions.setMultiSelectionActive); - const resetSelectedElements = useStoreActions((actions) => actions.resetSelectedElements); - const onNodesChange = useStoreState((state) => state.onNodesChange); - const onEdgesChange = useStoreState((state) => state.onEdgesChange); +export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => { + const store = useStoreApi(); + const { unsetNodesSelection, setMultiSelectionActive, resetSelectedElements, onNodesChange, onEdgesChange } = + useStore(selector); const deleteKeyPressed = useKeyPress(deleteKeyCode); const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode); diff --git a/src/hooks/useResizeHandler.ts b/src/hooks/useResizeHandler.ts index 164c7686..921aaef3 100644 --- a/src/hooks/useResizeHandler.ts +++ b/src/hooks/useResizeHandler.ts @@ -1,10 +1,13 @@ import { useEffect, MutableRefObject } from 'react'; -import { useStoreActions } from '../store/hooks'; +import { useStore } from '../store'; import { getDimensions } from '../utils'; +import { ReactFlowState } from '../types'; + +const updateSizeSelector = (state: ReactFlowState) => state.updateSize; export default (rendererNode: MutableRefObject) => { - const updateSize = useStoreActions((actions) => actions.updateSize); + const updateSize = useStore(updateSizeSelector); useEffect(() => { let resizeObserver: ResizeObserver; diff --git a/src/hooks/useUpdateNodeInternals.ts b/src/hooks/useUpdateNodeInternals.ts index 7d64af8a..69e978a2 100644 --- a/src/hooks/useUpdateNodeInternals.ts +++ b/src/hooks/useUpdateNodeInternals.ts @@ -1,13 +1,15 @@ import { useCallback } from 'react'; -import { useStoreActions } from '../store/hooks'; -import { ElementId, UpdateNodeInternals } from '../types'; +import { useStore } from '../store'; +import { ElementId, UpdateNodeInternals, ReactFlowState } from '../types'; + +const updateNodeDimsSelector = (state: ReactFlowState) => state.updateNodeDimensions; function useUpdateNodeInternals(): UpdateNodeInternals { - const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions); + const updateNodeDimensions = useStore(updateNodeDimsSelector); return useCallback((id: ElementId) => { - const nodeElement = document.querySelector(`.react-flow__node[data-id="${id}"]`); + const nodeElement = document.querySelector(`.react-flow__node[data-id="${id}"]`) as HTMLDivElement; if (nodeElement) { updateNodeDimensions([{ id, nodeElement, forceUpdate: true }]); diff --git a/src/hooks/useZoomPanHelper.ts b/src/hooks/useZoomPanHelper.ts index f5b91403..525e1064 100644 --- a/src/hooks/useZoomPanHelper.ts +++ b/src/hooks/useZoomPanHelper.ts @@ -1,9 +1,9 @@ import { useMemo } from 'react'; import { zoomIdentity } from 'd3-zoom'; -import { useStoreState, useStore } from '../store/hooks'; +import { useStoreApi, useStore } from '../store'; import { getRectOfNodes, pointToRendererPoint, getTransformForBounds } from '../utils/graph'; -import { FitViewParams, FlowTransform, ZoomPanHelperFunctions, Rect, XYPosition } from '../types'; +import { FitViewParams, FlowTransform, ZoomPanHelperFunctions, Rect, XYPosition, ReactFlowState } from '../types'; const DEFAULT_PADDING = 0.1; @@ -19,10 +19,14 @@ const initialZoomPanHelper: ZoomPanHelperFunctions = { initialized: false, }; +const selector = (s: ReactFlowState) => ({ + d3Zoom: s.d3Zoom, + d3Selection: s.d3Selection, +}); + const useZoomPanHelper = (): ZoomPanHelperFunctions => { - const store = useStore(); - const d3Zoom = useStoreState((s) => s.d3Zoom); - const d3Selection = useStoreState((s) => s.d3Selection); + const store = useStoreApi(); + const { d3Zoom, d3Selection } = useStore(selector); const zoomPanHelperFunctions = useMemo(() => { if (d3Selection && d3Zoom) { diff --git a/src/index.ts b/src/index.ts index b46051bf..38713781 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,7 +28,7 @@ export { default as useZoomPanHelper } from './hooks/useZoomPanHelper'; export { default as useUpdateNodeInternals } from './hooks/useUpdateNodeInternals'; export * from './additional-components'; -export * from './store/hooks'; +export { useStore, useStoreApi } from './store'; export * from './types'; export { ReactFlowProps } from './container/ReactFlow'; diff --git a/src/store/actions.ts b/src/store/actions.ts deleted file mode 100644 index 17ab0cee..00000000 --- a/src/store/actions.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { createAction } from './utils'; - -import { - Node, - Edge, - Elements, - OnConnectEndFunc, - OnConnectFunc, - OnConnectStartFunc, - OnConnectStopFunc, - NodeDimensionUpdate, - NodePosUpdate, - NodeDiffUpdate, - XYPosition, - Transform, - Dimensions, - InitD3ZoomPayload, - TranslateExtent, - SetConnectionId, - SnapGrid, - ConnectionMode, - NodeExtent, - OnElementsChange, -} from '../types'; - -import * as constants from './contants'; - -export const setOnConnect = (onConnect: OnConnectFunc) => - createAction(constants.SET_ON_CONNECT, { - onConnect, - }); - -export const setOnConnectStart = (onConnectStart: OnConnectStartFunc) => - createAction(constants.SET_ON_CONNECT_START, { - onConnectStart, - }); - -export const setOnConnectStop = (onConnectStop: OnConnectStopFunc) => - createAction(constants.SET_ON_CONNECT_STOP, { - onConnectStop, - }); - -export const setOnConnectEnd = (onConnectEnd: OnConnectEndFunc) => - createAction(constants.SET_ON_CONNECT_END, { - onConnectEnd, - }); - -export const setNodes = (nodes: Node[]) => createAction(constants.SET_NODES, nodes); -export const setEdges = (edges: Edge[]) => createAction(constants.SET_EDGES, edges); - -export const updateNodeDimensions = (updates: NodeDimensionUpdate[]) => - createAction(constants.UPDATE_NODE_DIMENSIONS, updates); - -export const updateNodePos = (payload: NodePosUpdate) => createAction(constants.UPDATE_NODE_POS, payload); - -export const updateNodePosDiff = (payload: NodeDiffUpdate) => createAction(constants.UPDATE_NODE_POS_DIFF, payload); - -export const setUserSelection = (mousePos: XYPosition) => createAction(constants.SET_USER_SELECTION, mousePos); - -export const updateUserSelection = (mousePos: XYPosition) => createAction(constants.UPDATE_USER_SELECTION, mousePos); - -export const unsetUserSelection = () => createAction(constants.UNSET_USER_SELECTION); - -export const setSelection = (selectionActive: boolean) => - createAction(constants.SET_SELECTION, { - selectionActive, - }); - -export const unsetNodesSelection = () => - createAction(constants.UNSET_NODES_SELECTION, { - nodesSelectionActive: false, - }); - -export const resetSelectedElements = () => - createAction(constants.RESET_SELECTED_ELEMENTS, { - selectedElements: null, - }); - -export const setSelectedElements = (elements: Elements) => createAction(constants.SET_SELECTED_ELEMENTS, elements); - -export const addSelectedElements = (elements: Elements) => createAction(constants.ADD_SELECTED_ELEMENTS, elements); - -export const updateTransform = (transform: Transform) => createAction(constants.UPDATE_TRANSFORM, { transform }); - -export const updateSize = (size: Dimensions) => - createAction(constants.UPDATE_SIZE, { - width: size.width || 500, - height: size.height || 500, - }); - -export const initD3Zoom = (payload: InitD3ZoomPayload) => createAction(constants.INIT_D3ZOOM, payload); - -export const setMinZoom = (minZoom: number) => createAction(constants.SET_MINZOOM, minZoom); - -export const setMaxZoom = (maxZoom: number) => createAction(constants.SET_MAXZOOM, maxZoom); - -export const setTranslateExtent = (translateExtent: TranslateExtent) => - createAction(constants.SET_TRANSLATEEXTENT, translateExtent); - -export const setConnectionPosition = (connectionPosition: XYPosition) => - createAction(constants.SET_CONNECTION_POSITION, { connectionPosition }); - -export const setConnectionNodeId = (payload: SetConnectionId) => createAction(constants.SET_CONNECTION_NODEID, payload); - -export const setSnapToGrid = (snapToGrid: boolean) => createAction(constants.SET_SNAPTOGRID, { snapToGrid }); - -export const setSnapGrid = (snapGrid: SnapGrid) => createAction(constants.SET_SNAPGRID, { snapGrid }); - -export const setInteractive = (isInteractive: boolean) => - createAction(constants.SET_INTERACTIVE, { - nodesDraggable: isInteractive, - nodesConnectable: isInteractive, - elementsSelectable: isInteractive, - }); - -export const setNodesDraggable = (nodesDraggable: boolean) => - createAction(constants.SET_NODES_DRAGGABLE, { nodesDraggable }); - -export const setNodesConnectable = (nodesConnectable: boolean) => - createAction(constants.SET_NODES_CONNECTABLE, { nodesConnectable }); - -export const setElementsSelectable = (elementsSelectable: boolean) => - createAction(constants.SET_ELEMENTS_SELECTABLE, { elementsSelectable }); - -export const setMultiSelectionActive = (multiSelectionActive: boolean) => - createAction(constants.SET_MULTI_SELECTION_ACTIVE, { multiSelectionActive }); - -export const setConnectionMode = (connectionMode: ConnectionMode) => - createAction(constants.SET_CONNECTION_MODE, { connectionMode }); - -export const setNodeExtent = (nodeExtent: NodeExtent) => createAction(constants.SET_NODE_EXTENT, nodeExtent); - -export const setOnNodesChange = (onNodesChange: OnElementsChange) => - createAction(constants.SET_ON_NODES_CHANGE, { onNodesChange }); - -export const setOnEdgesChange = (onEdgesChange: OnElementsChange) => - createAction(constants.SET_ON_EDGES_CHANGE, { onEdgesChange }); - -export type ReactFlowAction = ReturnType< - | typeof setOnConnect - | typeof setOnConnectStart - | typeof setOnConnectStop - | typeof setOnConnectEnd - | typeof setNodes - | typeof setEdges - | typeof updateNodeDimensions - | typeof updateNodePos - | typeof updateNodePosDiff - | typeof setUserSelection - | typeof updateUserSelection - | typeof unsetUserSelection - | typeof setSelection - | typeof unsetNodesSelection - | typeof resetSelectedElements - | typeof setSelectedElements - | typeof addSelectedElements - | typeof updateTransform - | typeof updateSize - | typeof initD3Zoom - | typeof setMinZoom - | typeof setMaxZoom - | typeof setTranslateExtent - | typeof setConnectionPosition - | typeof setConnectionNodeId - | typeof setSnapToGrid - | typeof setSnapGrid - | typeof setInteractive - | typeof setNodesDraggable - | typeof setNodesConnectable - | typeof setElementsSelectable - | typeof setMultiSelectionActive - | typeof setConnectionMode - | typeof setNodeExtent - | typeof setOnNodesChange - | typeof setOnEdgesChange ->; diff --git a/src/store/configure-store.ts b/src/store/configure-store.ts deleted file mode 100644 index 6005bdf1..00000000 --- a/src/store/configure-store.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createStore, applyMiddleware, Store } from 'redux'; -import thunk from 'redux-thunk'; - -import { ReactFlowState } from '../types'; -import { ReactFlowAction } from './actions'; -import reactFlowReducer from './reducer'; - -export default function configureStore(preloadedState: ReactFlowState): Store { - const store = createStore(reactFlowReducer, preloadedState, applyMiddleware(thunk)); - return store; -} diff --git a/src/store/contants.ts b/src/store/contants.ts deleted file mode 100644 index 890ada35..00000000 --- a/src/store/contants.ts +++ /dev/null @@ -1,36 +0,0 @@ -export const SET_ON_CONNECT = 'SET_ON_CONNECT'; -export const SET_ON_CONNECT_START = 'SET_ON_CONNECT_START'; -export const SET_ON_CONNECT_STOP = 'SET_ON_CONNECT_STOP'; -export const SET_ON_CONNECT_END = 'SET_ON_CONNECT_END'; -export const SET_NODES = 'SET_NODES'; -export const SET_EDGES = 'SET_EDGES'; -export const UPDATE_NODE_DIMENSIONS = 'UPDATE_NODE_DIMENSIONS'; -export const UPDATE_NODE_POS = 'UPDATE_NODE_POS'; -export const UPDATE_NODE_POS_DIFF = 'UPDATE_NODE_POS_DIFF'; -export const SET_USER_SELECTION = 'SET_USER_SELECTION'; -export const UPDATE_USER_SELECTION = 'UPDATE_USER_SELECTION'; -export const UNSET_USER_SELECTION = 'UNSET_USER_SELECTION'; -export const SET_SELECTION = 'SET_SELECTION'; -export const UNSET_NODES_SELECTION = 'UNSET_NODES_SELECTION'; -export const SET_SELECTED_ELEMENTS = 'SET_SELECTED_ELEMENTS'; -export const RESET_SELECTED_ELEMENTS = 'RESET_SELECTED_ELEMENTS'; -export const ADD_SELECTED_ELEMENTS = 'ADD_SELECTED_ELEMENTS'; -export const UPDATE_TRANSFORM = 'UPDATE_TRANSFORM'; -export const UPDATE_SIZE = 'UPDATE_SIZE'; -export const INIT_D3ZOOM = 'INIT_D3ZOOM'; -export const SET_MINZOOM = 'SET_MINZOOM'; -export const SET_MAXZOOM = 'SET_MAXZOOM'; -export const SET_TRANSLATEEXTENT = 'SET_TRANSLATEEXTENT'; -export const SET_CONNECTION_POSITION = 'SET_CONNECTION_POSITION'; -export const SET_CONNECTION_NODEID = 'SET_CONNECTION_NODEID'; -export const SET_SNAPTOGRID = 'SET_SNAPTOGRID'; -export const SET_SNAPGRID = 'SET_SNAPGRID'; -export const SET_INTERACTIVE = 'SET_INTERACTIVE'; -export const SET_NODES_DRAGGABLE = 'SET_NODES_DRAGGABLE'; -export const SET_NODES_CONNECTABLE = 'SET_NODES_CONNECTABLE'; -export const SET_ELEMENTS_SELECTABLE = 'SET_ELEMENTS_SELECTABLE'; -export const SET_MULTI_SELECTION_ACTIVE = 'SET_MULTI_SELECTION_ACTIVE'; -export const SET_CONNECTION_MODE = 'SET_CONNECTION_MODE'; -export const SET_NODE_EXTENT = 'SET_NODE_EXTENT'; -export const SET_ON_NODES_CHANGE = 'SET_ON_NODES_CHANGE'; -export const SET_ON_EDGES_CHANGE = 'SET_ON_EDGES_CHANGE'; diff --git a/src/store/hooks.ts b/src/store/hooks.ts deleted file mode 100644 index 4ee7affc..00000000 --- a/src/store/hooks.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { bindActionCreators, Store, ActionCreator, ActionCreatorsMapObject } from 'redux'; -import { - useStore as useStoreRedux, - useSelector, - useDispatch as reduxUseDispatch, - TypedUseSelectorHook, -} from 'react-redux'; -import { useMemo } from 'react'; - -import { ReactFlowDispatch } from './index'; -import * as actions from './actions'; -import { ReactFlowAction } from './actions'; -import { ReactFlowState } from '../types'; - -export const useTypedSelector: TypedUseSelectorHook = useSelector; - -export type ActionCreatorSelector = (acts: typeof actions) => ActionCreator; -export type ActionMapObjectSelector = (acts: typeof actions) => ActionCreatorsMapObject; -export type ActionSelector = (acts: typeof actions) => ActionCreatorsMapObject | ActionCreator; - -export function useStoreActions( - actionSelector: ActionCreatorSelector -): ActionCreator; - -export function useStoreActions( - actionSelector: ActionMapObjectSelector -): ActionCreatorsMapObject; - -export function useStoreActions(actionSelector: ActionSelector) { - const dispatch: ReactFlowDispatch = reduxUseDispatch(); - const currAction = actionSelector(actions); - - const action = useMemo(() => { - // this looks weird but required if both ActionSelector and ActionMapObjectSelector are supported - return typeof currAction === 'function' - ? bindActionCreators(currAction, dispatch) - : bindActionCreators(currAction, dispatch); - }, [dispatch, currAction]); - - return action; -} - -export const useStoreState = useTypedSelector; -export const useStore = (): Store => { - const store = useStoreRedux(); - return store; -}; -export const useDispatch: ReactFlowDispatch = reduxUseDispatch; diff --git a/src/store/index.ts b/src/store/index.ts index fa2eda77..9bcfcbad 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,66 +1,410 @@ -import configureStore from './configure-store'; +import create from 'zustand'; +import createContext from 'zustand/context'; +import isEqual from 'fast-deep-equal'; -import { ReactFlowState, ConnectionMode } from '../types'; +import { clampPosition, getDimensions } from '../utils'; +import { + ReactFlowState, + ConnectionMode, + Node, + Edge, + ElementChange, + NodeDimensionUpdate, + NodeDiffUpdate, + XYPosition, + Elements, + InitD3ZoomPayload, + TranslateExtent, + NodeExtent, + Transform, + Dimensions, + OnConnectFunc, + OnConnectStartFunc, + OnConnectStopFunc, + OnConnectEndFunc, + SetConnectionId, + SnapGrid, + OnElementsChange, +} from '../types'; +import { parseNode, parseEdge, isNode, getRectOfNodes, getNodesInside, getConnectedEdges } from '../utils/graph'; +import { getSourceTargetNodes } from '../container/EdgeRenderer/utils'; +import { getHandleBounds } from '../components/Nodes/utils'; -export const initialState: ReactFlowState = { - width: 0, - height: 0, - transform: [0, 0, 1], - nodes: [], - edges: [], - onNodesChange: null, - onEdgesChange: null, +const { Provider, useStore, useStoreApi } = createContext(); - selectedElements: null, - selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 }, - - d3Zoom: null, - d3Selection: null, - d3ZoomHandler: undefined, - minZoom: 0.5, - maxZoom: 2, - translateExtent: [ - [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY], - [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY], - ], - - nodeExtent: [ - [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY], - [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY], - ], - - nodesSelectionActive: false, - selectionActive: false, - - userSelectionRect: { - startX: 0, - startY: 0, - x: 0, - y: 0, +const createStore = () => + create((set, get) => ({ width: 0, height: 0, - draw: false, - }, - connectionNodeId: null, - connectionHandleId: null, - connectionHandleType: 'source', - connectionPosition: { x: 0, y: 0 }, - connectionMode: ConnectionMode.Strict, + transform: [0, 0, 1], + nodes: [], + edges: [], + onNodesChange: null, + onEdgesChange: null, - snapGrid: [15, 15], - snapToGrid: false, + selectedElements: null, + selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 }, - nodesDraggable: true, - nodesConnectable: true, - elementsSelectable: true, + d3Zoom: null, + d3Selection: null, + d3ZoomHandler: undefined, + minZoom: 0.5, + maxZoom: 2, + translateExtent: [ + [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY], + [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY], + ], - multiSelectionActive: false, + nodeExtent: [ + [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY], + [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY], + ], - reactFlowVersion: typeof __REACT_FLOW_VERSION__ !== 'undefined' ? __REACT_FLOW_VERSION__ : '-', -}; + nodesSelectionActive: false, + selectionActive: false, -const store = configureStore(initialState); + userSelectionRect: { + startX: 0, + startY: 0, + x: 0, + y: 0, + width: 0, + height: 0, + draw: false, + }, + connectionNodeId: null, + connectionHandleId: null, + connectionHandleType: 'source', + connectionPosition: { x: 0, y: 0 }, + connectionMode: ConnectionMode.Strict, -export type ReactFlowDispatch = typeof store.dispatch; + snapGrid: [15, 15], + snapToGrid: false, -export default store; + nodesDraggable: true, + nodesConnectable: true, + elementsSelectable: true, + + multiSelectionActive: false, + + reactFlowVersion: typeof __REACT_FLOW_VERSION__ !== 'undefined' ? __REACT_FLOW_VERSION__ : '-', + + setNodes: (propNodes: Node[]) => { + const { nodes, edges, nodeExtent } = get(); + const nextNodes = propNodes.map((propNode: Node) => { + const storeNode = nodes.find((node) => node.id === propNode.id); + + if (storeNode) { + if (typeof propNode.type !== 'undefined' && propNode.type !== storeNode.type) { + const updatedNode: Node = { + ...storeNode, + ...propNode, + }; + // we reset the elements dimensions here in order to force a re-calculation of the bounds. + // When the type of a node changes it is possible that the number or positions of handles changes too. + updatedNode.width = null; + return updatedNode; + } + } + + return parseNode(propNode, nodeExtent); + }); + + const updatedEdges = edges.map((edge) => { + const { sourceNode, targetNode } = getSourceTargetNodes(edge, nextNodes); + + if (sourceNode) { + edge.sourceNode = sourceNode; + } + if (targetNode) { + edge.targetNode = targetNode; + } + + return edge; + }); + + set({ + nodes: nextNodes, + edges: updatedEdges, + }); + }, + setEdges: (propEdges: Edge[]) => { + const { edges, nodes } = get(); + + const nextEdges = propEdges.map((propEdge: Edge) => { + const storeEdge = edges.find((se) => se.id === propEdge.id); + + if (storeEdge) { + return parseEdge(propEdge); + } else { + const parsedEdge = parseEdge(propEdge); + const { sourceNode, targetNode } = getSourceTargetNodes(parsedEdge, nodes); + + if (sourceNode) { + parsedEdge.sourceNode = sourceNode; + } + if (targetNode) { + parsedEdge.targetNode = targetNode; + } + + return parsedEdge; + } + }); + + set({ edges: nextEdges }); + }, + updateNodeDimensions: (updates: NodeDimensionUpdate[]) => { + const { onNodesChange, nodes, transform } = get(); + + const initialChanges: ElementChange[] = []; + const nodesToChange: ElementChange[] = nodes.reduce((res, node) => { + const update = updates.find((u) => u.id === node.id); + if (update) { + const dimensions = getDimensions(update.nodeElement); + const doUpdate = + dimensions.width && + dimensions.height && + (node.width !== dimensions.width || node.height !== dimensions.height || update.forceUpdate); + + if (doUpdate) { + const handleBounds = getHandleBounds(update.nodeElement, transform[2]); + const change = { + id: node.id, + change: { + ...dimensions, + handleBounds, + }, + } as ElementChange; + + res.push(change); + } + } + + return res; + }, initialChanges); + + if (onNodesChange) { + onNodesChange(nodesToChange); + } + }, + updateNodePosDiff: ({ id, diff, isDragging }: NodeDiffUpdate) => { + const { onNodesChange, nodes } = get(); + + if (onNodesChange && id && diff) { + const matchingNode = nodes.find((n) => n.id === id); + + if (matchingNode) { + requestAnimationFrame(() => + onNodesChange([ + { + id, + change: { + position: { + x: matchingNode.position.x + diff.x, + y: matchingNode.position.y + diff.y, + isDragging, + }, + }, + }, + ]) + ); + } + } + }, + setUserSelection: (mousePos: XYPosition) => { + set({ + selectionActive: true, + userSelectionRect: { + width: 0, + height: 0, + startX: mousePos.x, + startY: mousePos.y, + x: mousePos.x, + y: mousePos.y, + draw: true, + }, + }); + }, + updateUserSelection: (mousePos: XYPosition) => { + const { userSelectionRect, nodes, edges, transform, selectedElements } = get(); + const startX = userSelectionRect.startX ?? 0; + const startY = userSelectionRect.startY ?? 0; + + const nextUserSelectRect = { + ...userSelectionRect, + x: mousePos.x < startX ? mousePos.x : userSelectionRect.x, + y: mousePos.y < startY ? mousePos.y : userSelectionRect.y, + width: Math.abs(mousePos.x - startX), + height: Math.abs(mousePos.y - startY), + }; + + const selectedNodes = getNodesInside(nodes, nextUserSelectRect, transform, false, true); + const selectedEdges = getConnectedEdges(selectedNodes, edges); + + const nextSelectedElements = [...selectedNodes, ...selectedEdges]; + const selectedElementsChanged = !isEqual(nextSelectedElements, selectedElements); + + if (selectedElementsChanged) { + set({ + selectedElements: nextSelectedElements.length > 0 ? nextSelectedElements : null, + userSelectionRect: nextUserSelectRect, + }); + } else { + set({ + userSelectionRect: nextUserSelectRect, + }); + } + }, + unsetUserSelection: () => { + const { selectedElements, userSelectionRect } = get(); + const selectedNodes = selectedElements?.filter((node) => isNode(node) && node.position) as Node[]; + + const stateUpdate = { + selectionActive: false, + userSelectionRect: { + ...userSelectionRect, + draw: false, + }, + selectedElements: null, + selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 }, + nodesSelectionActive: false, + }; + + if (selectedNodes && selectedNodes.length > 0) { + const selectedNodesBbox = getRectOfNodes(selectedNodes); + stateUpdate.selectedNodesBbox = selectedNodesBbox; + stateUpdate.nodesSelectionActive = true; + } + + set(stateUpdate); + }, + setSelectedElements: (elements: Elements) => { + const { selectedElements } = get(); + const selectedElementsArr = Array.isArray(elements) ? elements : [elements]; + const selectedElementsUpdated = !isEqual(selectedElementsArr, selectedElements); + + set({ + selectedElements: selectedElementsUpdated ? selectedElementsArr : selectedElements, + }); + }, + addSelectedElements: (elements: Elements) => { + const { multiSelectionActive, selectedElements } = get(); + const selectedElementsArr = Array.isArray(elements) ? elements : [elements]; + + let nextElements = selectedElementsArr; + + if (multiSelectionActive) { + nextElements = selectedElements ? [...selectedElements, ...selectedElementsArr] : selectedElementsArr; + } + + const selectedElementsUpdated = !isEqual(nextElements, selectedElements); + + set({ selectedElements: selectedElementsUpdated ? nextElements : selectedElements }); + }, + initD3Zoom: ({ d3Zoom, d3Selection, d3ZoomHandler, transform }: InitD3ZoomPayload) => { + set({ + d3Zoom, + d3Selection, + d3ZoomHandler, + transform, + }); + }, + setMinZoom: (minZoom: number) => { + const { d3Zoom, maxZoom } = get(); + d3Zoom?.scaleExtent([minZoom, maxZoom]); + + set({ minZoom }); + }, + setMaxZoom: (maxZoom: number) => { + const { d3Zoom, minZoom } = get(); + d3Zoom?.scaleExtent([minZoom, maxZoom]); + + set({ maxZoom }); + }, + setTranslateExtent: (translateExtent: TranslateExtent) => { + const { d3Zoom } = get(); + d3Zoom?.translateExtent(translateExtent); + + set({ translateExtent }); + }, + setNodeExtent: (nodeExtent: NodeExtent) => { + set({ + nodeExtent, + nodes: get().nodes.map((node) => { + return { + ...node, + position: clampPosition(node.position, nodeExtent), + __rf: { + ...node.__rf, + }, + }; + }), + }); + }, + unsetNodesSelection: () => { + set({ nodesSelectionActive: false }); + }, + resetSelectedElements: () => { + set({ selectedElements: null }); + }, + updateTransform: (transform: Transform) => { + set({ transform }); + }, + updateSize: (size: Dimensions) => { + set({ width: size.width || 500, height: size.height || 500 }); + }, + setOnConnect: (onConnect: OnConnectFunc) => { + set({ onConnect }); + }, + setOnConnectStart: (onConnectStart: OnConnectStartFunc) => { + set({ onConnectStart }); + }, + setOnConnectStop: (onConnectStop: OnConnectStopFunc) => { + set({ onConnectStop }); + }, + setOnConnectEnd: (onConnectEnd: OnConnectEndFunc) => { + set({ onConnectEnd }); + }, + setConnectionPosition: (connectionPosition: XYPosition) => { + set({ connectionPosition }); + }, + setConnectionNodeId: (params: SetConnectionId) => { + set({ ...params }); + }, + setSnapToGrid: (snapToGrid: boolean) => { + set({ snapToGrid }); + }, + setSnapGrid: (snapGrid: SnapGrid) => { + set({ snapGrid }); + }, + setInteractive: (isInteractive: boolean) => { + set({ + nodesDraggable: isInteractive, + nodesConnectable: isInteractive, + elementsSelectable: isInteractive, + }); + }, + setNodesDraggable: (nodesDraggable: boolean) => { + set({ nodesDraggable }); + }, + setNodesConnectable: (nodesConnectable: boolean) => { + set({ nodesConnectable }); + }, + setElementsSelectable: (elementsSelectable: boolean) => { + set({ elementsSelectable }); + }, + setMultiSelectionActive: (multiSelectionActive: boolean) => { + set({ multiSelectionActive }); + }, + setConnectionMode: (connectionMode: ConnectionMode) => { + set({ connectionMode }); + }, + setOnNodesChange: (onNodesChange: OnElementsChange) => { + set({ onNodesChange }); + }, + setOnEdgesChange: (onEdgesChange: OnElementsChange) => { + set({ onEdgesChange }); + }, + })); + +export { Provider, useStore, createStore, useStoreApi }; diff --git a/src/store/reducer.ts b/src/store/reducer.ts deleted file mode 100644 index f5b7b18f..00000000 --- a/src/store/reducer.ts +++ /dev/null @@ -1,350 +0,0 @@ -import isEqual from 'fast-deep-equal'; - -import { clampPosition, getDimensions } from '../utils'; -import { getNodesInside, getConnectedEdges, getRectOfNodes, isNode, parseNode, parseEdge } from '../utils/graph'; -import { getHandleBounds } from '../components/Nodes/utils'; -import { getSourceTargetNodes } from '../container/EdgeRenderer/utils'; - -import { ReactFlowState, Node, XYPosition, Edge, ElementChange } from '../types'; -import * as constants from './contants'; -import { ReactFlowAction } from './actions'; - -import { initialState } from './index'; - -export default function reactFlowReducer(state = initialState, action: ReactFlowAction): ReactFlowState { - switch (action.type) { - case constants.SET_NODES: { - const propNodes = action.payload; - const nextNodes = propNodes.map((propNode: Node) => { - const storeNode = state.nodes.find((node) => node.id === propNode.id); - - if (storeNode) { - if (typeof propNode.type !== 'undefined' && propNode.type !== storeNode.type) { - const updatedNode: Node = { - ...storeNode, - ...propNode, - }; - // we reset the elements dimensions here in order to force a re-calculation of the bounds. - // When the type of a node changes it is possible that the number or positions of handles changes too. - updatedNode.width = null; - return updatedNode; - } - } - - return parseNode(propNode, state.nodeExtent); - }); - - const updatedEdges = state.edges.map((edge) => { - const { sourceNode, targetNode } = getSourceTargetNodes(edge, nextNodes); - - if (sourceNode) { - edge.sourceNode = sourceNode; - } - if (targetNode) { - edge.targetNode = targetNode; - } - - return edge; - }); - - return { ...state, nodes: nextNodes, edges: updatedEdges }; - } - case constants.SET_EDGES: { - const propElements = action.payload; - const nextEdges = propElements.map((propEdge: Edge) => { - const storeEdge = state.edges.find((se) => se.id === propEdge.id); - - if (storeEdge) { - return parseEdge(propEdge); - } else { - const parsedEdge = parseEdge(propEdge); - const { sourceNode, targetNode } = getSourceTargetNodes(parsedEdge, state.nodes); - - if (sourceNode) { - parsedEdge.sourceNode = sourceNode; - } - if (targetNode) { - parsedEdge.targetNode = targetNode; - } - - return parsedEdge; - } - }); - - return { ...state, edges: nextEdges }; - } - case constants.UPDATE_NODE_DIMENSIONS: { - const initialChanges: ElementChange[] = []; - const nodesToChange: ElementChange[] = state.nodes.reduce((res, node) => { - const update = action.payload.find((u) => u.id === node.id); - if (update) { - const dimensions = getDimensions(update.nodeElement); - const doUpdate = - dimensions.width && - dimensions.height && - (node.width !== dimensions.width || node.height !== dimensions.height || update.forceUpdate); - - if (doUpdate) { - const handleBounds = getHandleBounds(update.nodeElement, state.transform[2]); - const change = { - id: node.id, - change: { - ...dimensions, - handleBounds, - }, - } as ElementChange; - - res.push(change); - } - } - - return res; - }, initialChanges); - - if (state.onNodesChange) { - requestAnimationFrame(() => state.onNodesChange?.(nodesToChange)); - } - - return state; - } - case constants.UPDATE_NODE_POS: { - const { id, pos } = action.payload; - let position: XYPosition = pos; - - if (state.snapToGrid) { - const [gridSizeX, gridSizeY] = state.snapGrid; - position = { - x: gridSizeX * Math.round(pos.x / gridSizeX), - y: gridSizeY * Math.round(pos.y / gridSizeY), - }; - } - - if (state.onNodesChange) { - state.onNodesChange([{ id, change: { position } }]); - - return state; - } - - const nextNodes = state.nodes.map((node) => { - if (node.id === id) { - return { - ...node, - position, - - __rf: { - ...node.__rf, - }, - }; - } - - return node; - }); - - return { ...state, nodes: nextNodes }; - } - case constants.UPDATE_NODE_POS_DIFF: { - const { id, diff, isDragging } = action.payload; - - if (state.onNodesChange && id && diff) { - const matchingNode = state.nodes.find((n) => n.id === id); - - if (matchingNode) { - requestAnimationFrame(() => - state.onNodesChange?.([ - { - id, - change: { - position: { - x: matchingNode.position.x + diff.x, - y: matchingNode.position.y + diff.y, - isDragging, - }, - }, - }, - ]) - ); - } - } - - return state; - } - case constants.SET_USER_SELECTION: { - const mousePos = action.payload; - - return { - ...state, - selectionActive: true, - userSelectionRect: { - width: 0, - height: 0, - startX: mousePos.x, - startY: mousePos.y, - x: mousePos.x, - y: mousePos.y, - draw: true, - }, - }; - } - case constants.UPDATE_USER_SELECTION: { - const mousePos = action.payload; - const startX = state.userSelectionRect.startX ?? 0; - const startY = state.userSelectionRect.startY ?? 0; - - const nextUserSelectRect = { - ...state.userSelectionRect, - x: mousePos.x < startX ? mousePos.x : state.userSelectionRect.x, - y: mousePos.y < startY ? mousePos.y : state.userSelectionRect.y, - width: Math.abs(mousePos.x - startX), - height: Math.abs(mousePos.y - startY), - }; - - const selectedNodes = getNodesInside(state.nodes, nextUserSelectRect, state.transform, false, true); - const selectedEdges = getConnectedEdges(selectedNodes, state.edges); - - const nextSelectedElements = [...selectedNodes, ...selectedEdges]; - const selectedElementsChanged = !isEqual(nextSelectedElements, state.selectedElements); - const selectedElementsUpdate = selectedElementsChanged - ? { - selectedElements: nextSelectedElements.length > 0 ? nextSelectedElements : null, - } - : {}; - - return { - ...state, - ...selectedElementsUpdate, - userSelectionRect: nextUserSelectRect, - }; - } - case constants.UNSET_USER_SELECTION: { - const selectedNodes = state.selectedElements?.filter((node) => isNode(node) && node.__rf) as Node[]; - - const stateUpdate = { - ...state, - selectionActive: false, - userSelectionRect: { - ...state.userSelectionRect, - draw: false, - }, - }; - - if (!selectedNodes || selectedNodes.length === 0) { - stateUpdate.selectedElements = null; - stateUpdate.nodesSelectionActive = false; - } else { - const selectedNodesBbox = getRectOfNodes(selectedNodes); - stateUpdate.selectedNodesBbox = selectedNodesBbox; - stateUpdate.nodesSelectionActive = true; - } - - return stateUpdate; - } - case constants.SET_SELECTED_ELEMENTS: { - const elements = action.payload; - const selectedElementsArr = Array.isArray(elements) ? elements : [elements]; - const selectedElementsUpdated = !isEqual(selectedElementsArr, state.selectedElements); - const selectedElements = selectedElementsUpdated ? selectedElementsArr : state.selectedElements; - - return { - ...state, - selectedElements, - }; - } - case constants.ADD_SELECTED_ELEMENTS: { - const { multiSelectionActive, selectedElements } = state; - const elements = action.payload; - const selectedElementsArr = Array.isArray(elements) ? elements : [elements]; - - let nextElements = selectedElementsArr; - - if (multiSelectionActive) { - nextElements = selectedElements ? [...selectedElements, ...selectedElementsArr] : selectedElementsArr; - } - - const selectedElementsUpdated = !isEqual(nextElements, state.selectedElements); - const nextSelectedElements = selectedElementsUpdated ? nextElements : state.selectedElements; - - return { ...state, selectedElements: nextSelectedElements }; - } - case constants.INIT_D3ZOOM: { - const { d3Zoom, d3Selection, d3ZoomHandler, transform } = action.payload; - - return { - ...state, - d3Zoom, - d3Selection, - d3ZoomHandler, - transform, - }; - } - case constants.SET_MINZOOM: { - const minZoom = action.payload; - - state.d3Zoom?.scaleExtent([minZoom, state.maxZoom]); - - return { - ...state, - minZoom, - }; - } - - case constants.SET_MAXZOOM: { - const maxZoom = action.payload; - - state.d3Zoom?.scaleExtent([state.minZoom, maxZoom]); - - return { - ...state, - maxZoom, - }; - } - case constants.SET_TRANSLATEEXTENT: { - const translateExtent = action.payload; - - state.d3Zoom?.translateExtent(translateExtent); - - return { - ...state, - translateExtent, - }; - } - case constants.SET_NODE_EXTENT: { - const nodeExtent = action.payload; - return { - ...state, - nodeExtent, - nodes: state.nodes.map((node) => { - return { - ...node, - position: clampPosition(node.position, nodeExtent), - __rf: { - ...node.__rf, - }, - }; - }), - }; - } - case constants.SET_ON_CONNECT: - case constants.SET_ON_CONNECT_START: - case constants.SET_ON_CONNECT_STOP: - case constants.SET_ON_CONNECT_END: - case constants.RESET_SELECTED_ELEMENTS: - case constants.UNSET_NODES_SELECTION: - case constants.UPDATE_TRANSFORM: - case constants.UPDATE_SIZE: - case constants.SET_CONNECTION_POSITION: - case constants.SET_CONNECTION_NODEID: - case constants.SET_SNAPTOGRID: - case constants.SET_SNAPGRID: - case constants.SET_INTERACTIVE: - case constants.SET_NODES_DRAGGABLE: - case constants.SET_NODES_CONNECTABLE: - case constants.SET_ELEMENTS_SELECTABLE: - case constants.SET_MULTI_SELECTION_ACTIVE: - case constants.SET_CONNECTION_MODE: - case constants.SET_ON_NODES_CHANGE: - case constants.SET_ON_EDGES_CHANGE: - return { ...state, ...action.payload }; - default: - return state; - } -} diff --git a/src/store/utils.ts b/src/store/utils.ts deleted file mode 100644 index c4bfbaf3..00000000 --- a/src/store/utils.ts +++ /dev/null @@ -1,5 +0,0 @@ -export function createAction(type: T): { type: T }; -export function createAction(type: T, payload: P): { type: T; payload: P }; -export function createAction(type: string, payload?: any) { - return { type, payload }; -} diff --git a/src/types/index.ts b/src/types/index.ts index 6de72b6d..9974c501 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -453,6 +453,41 @@ export interface ReactFlowState { reactFlowVersion: string; + setNodes: (nodes: Node[]) => void; + setEdges: (edges: Edge[]) => void; + updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void; + updateNodePosDiff: (update: NodeDiffUpdate) => void; + setUserSelection: (mousePos: XYPosition) => void; + updateUserSelection: (mousePos: XYPosition) => void; + unsetUserSelection: () => void; + unsetNodesSelection: () => void; + resetSelectedElements: () => void; + setSelectedElements: (elements: Elements) => void; + addSelectedElements: (elements: Elements) => void; + updateTransform: (transform: Transform) => void; + updateSize: (size: Dimensions) => void; + initD3Zoom: (payload: InitD3ZoomPayload) => void; + setMinZoom: (minZoom: number) => void; + setMaxZoom: (maxZoom: number) => void; + setTranslateExtent: (translateExtent: TranslateExtent) => void; + setNodeExtent: (nodeExtent: NodeExtent) => void; + setOnConnect: (onConnectFunction: OnConnectFunc) => void; + setOnConnectStart: (onConnectFunction: OnConnectStartFunc) => void; + setOnConnectStop: (onConnectFunction: OnConnectStopFunc) => void; + setOnConnectEnd: (onConnectFunction: OnConnectEndFunc) => void; + setConnectionPosition: (connectionPosition: XYPosition) => void; + setConnectionNodeId: (payload: SetConnectionId) => void; + setSnapToGrid: (snapToGrid: boolean) => void; + setSnapGrid: (snapGrid: SnapGrid) => void; + setInteractive: (isInteractive: boolean) => void; + setNodesDraggable: (nodesDraggable: boolean) => void; + setNodesConnectable: (nodesConnectable: boolean) => void; + setElementsSelectable: (elementsSelectable: boolean) => void; + setMultiSelectionActive: (multiSelectionActive: boolean) => void; + setConnectionMode: (connectionMode: ConnectionMode) => void; + setOnNodesChange: (onNodesChange: OnElementsChange) => void; + setOnEdgesChange: (onEdgesChange: OnElementsChange) => void; + onConnect?: OnConnectFunc; onConnectStart?: OnConnectStartFunc; onConnectStop?: OnConnectStopFunc; diff --git a/src/utils/graph.ts b/src/utils/graph.ts index 86d13a6e..8e84b139 100644 --- a/src/utils/graph.ts +++ b/src/utils/graph.ts @@ -1,6 +1,7 @@ -import { Store } from 'redux'; +import { GetState } from 'zustand'; import { clampPosition, clamp } from '../utils'; +import { ReactFlowState } from '../types'; import { ElementId, @@ -13,7 +14,6 @@ import { Box, Connection, FlowExportObject, - ReactFlowState, NodeExtent, ElementChange, } from '../types'; @@ -139,9 +139,9 @@ export const pointToRendererPoint = ( return position; }; -export const onLoadProject = (currentStore: Store) => { +export const onLoadProject = (getState: GetState) => { return (position: XYPosition): XYPosition => { - const { transform, snapToGrid, snapGrid } = currentStore.getState(); + const { transform, snapToGrid, snapGrid } = getState(); return pointToRendererPoint(position, transform, snapToGrid, snapGrid); }; @@ -256,17 +256,17 @@ const parseElements = (nodes: Node[], edges: Edge[]): Elements => { return [...nodes.map((n) => ({ ...n })), ...edges.map((e) => ({ ...e }))]; }; -export const onLoadGetElements = (currentStore: Store) => { +export const onLoadGetElements = (getState: GetState) => { return (): Elements => { - const { nodes = [], edges = [] } = currentStore.getState(); + const { nodes = [], edges = [] } = getState(); return parseElements(nodes, edges); }; }; -export const onLoadToObject = (currentStore: Store) => { +export const onLoadToObject = (getState: GetState) => { return (): FlowExportObject => { - const { nodes = [], edges = [], transform } = currentStore.getState(); + const { nodes = [], edges = [], transform } = getState(); return { elements: parseElements(nodes, edges),