From 88582a86cc048f686929571bacd7a772cffaecca Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 9 Dec 2021 07:24:45 +0100 Subject: [PATCH] refactor(general): fit view in store to prevent flickering, rename onLoad => onPaneReady --- example/src/Basic/index.tsx | 9 ++-- example/src/CustomNode/index.tsx | 9 ++-- example/src/FloatingEdges/index.tsx | 6 +-- example/src/Layouting/index.tsx | 6 +-- example/src/NestedNodes/index.tsx | 8 +-- example/src/Stress/index.tsx | 12 +++-- example/src/UpdatableEdge/index.tsx | 6 +-- src/components/StoreUpdater/index.tsx | 8 +++ src/components/ViewFitter/index.tsx | 51 ------------------- src/container/GraphView/index.tsx | 6 +-- src/container/ReactFlow/index.tsx | 13 +++-- ...oadHandler.ts => useOnPaneReadyHandler.ts} | 14 ++--- src/store/index.ts | 9 ++-- src/store/initialState.ts | 3 ++ src/store/utils.ts | 26 ++++++++++ src/types/general.ts | 8 ++- src/types/nodes.ts | 2 +- 17 files changed, 96 insertions(+), 100 deletions(-) delete mode 100644 src/components/ViewFitter/index.tsx rename src/hooks/{useOnLoadHandler.ts => useOnPaneReadyHandler.ts} (84%) diff --git a/example/src/Basic/index.tsx b/example/src/Basic/index.tsx index eee70449..97a113b2 100644 --- a/example/src/Basic/index.tsx +++ b/example/src/Basic/index.tsx @@ -7,7 +7,7 @@ import ReactFlow, { Controls, Node, Edge, - OnLoadParams, + ReactFlowInstance, Connection, MarkerType, useNodesState, @@ -105,7 +105,7 @@ const nodeTypes = { }; const BasicFlow = () => { - const [rfInstance, setRfInstance] = useState(null); + const [rfInstance, setRfInstance] = useState(null); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); @@ -114,7 +114,7 @@ const BasicFlow = () => { return addEdge(connection, eds); }); }, []); - const onLoad = useCallback((reactFlowInstance: OnLoadParams) => setRfInstance(reactFlowInstance), []); + const onPaneReady = useCallback((reactFlowInstance: ReactFlowInstance) => setRfInstance(reactFlowInstance), []); const updatePos = () => { setNodes((nds) => { @@ -154,7 +154,7 @@ const BasicFlow = () => { { maxZoom={4} onlyRenderVisibleElements={false} nodeTypes={nodeTypes} + fitViewOnInit > diff --git a/example/src/CustomNode/index.tsx b/example/src/CustomNode/index.tsx index e8e80922..578f68b7 100644 --- a/example/src/CustomNode/index.tsx +++ b/example/src/CustomNode/index.tsx @@ -6,7 +6,7 @@ import ReactFlow, { MiniMap, Controls, Node, - OnLoadParams, + ReactFlowInstance, Position, SnapGrid, Connection, @@ -16,7 +16,7 @@ import ReactFlow, { import ColorSelectorNode from './ColorSelectorNode'; -const onLoad = (reactFlowInstance: OnLoadParams) => { +const onPaneReady = (reactFlowInstance: ReactFlowInstance) => { console.log('flow loaded:', reactFlowInstance); reactFlowInstance.fitView(); }; @@ -111,12 +111,13 @@ const CustomNodeFlow = () => { onConnect={onConnect} onNodeDragStop={onNodeDragStop} style={{ background: bgColor }} - onLoad={onLoad} + onPaneReady={onPaneReady} nodeTypes={nodeTypes} connectionLineStyle={connectionLineStyle} snapToGrid={true} snapGrid={snapGrid} defaultZoom={1.5} + fitViewOnInit > { @@ -137,4 +138,4 @@ const CustomNodeFlow = () => { ); }; -export default CustomNodeFlow; \ No newline at end of file +export default CustomNodeFlow; diff --git a/example/src/FloatingEdges/index.tsx b/example/src/FloatingEdges/index.tsx index c489ec0b..a9cf917c 100644 --- a/example/src/FloatingEdges/index.tsx +++ b/example/src/FloatingEdges/index.tsx @@ -3,7 +3,7 @@ import { useCallback } from 'react'; import ReactFlow, { addEdge, Background, - OnLoadParams, + ReactFlowInstance, EdgeTypesType, Connection, useNodesState, @@ -16,7 +16,7 @@ import FloatingEdge from './FloatingEdge'; import FloatingConnectionLine from './FloatingConnectionLine'; import { createElements } from './utils'; -const onLoad = (reactFlowInstance: OnLoadParams) => reactFlowInstance.fitView(); +const onPaneReady = (reactFlowInstance: ReactFlowInstance) => reactFlowInstance.fitView(); const { nodes: initialNodes, edges: initialEdges } = createElements(); @@ -40,7 +40,7 @@ const FloatingEdges = () => { onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onConnect={onConnect} - onLoad={onLoad} + onPaneReady={onPaneReady} edgeTypes={edgeTypes} connectionLineComponent={FloatingConnectionLine} > diff --git a/example/src/Layouting/index.tsx b/example/src/Layouting/index.tsx index a4fc53c7..4f7868cf 100644 --- a/example/src/Layouting/index.tsx +++ b/example/src/Layouting/index.tsx @@ -2,11 +2,7 @@ import { useCallback } from 'react'; import ReactFlow, { ReactFlowProvider, addEdge, - applyNodeChanges, - applyEdgeChanges, Controls, - NodeChange, - EdgeChange, Connection, CoordinateExtent, Position, @@ -71,7 +67,7 @@ const LayoutFlow = () => { edges={edges} onConnect={onConnect} nodeExtent={nodeExtent} - onLoad={() => onLayout('TB')} + onPaneReady={() => onLayout('TB')} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} > diff --git a/example/src/NestedNodes/index.tsx b/example/src/NestedNodes/index.tsx index bf46d158..1e4115ac 100644 --- a/example/src/NestedNodes/index.tsx +++ b/example/src/NestedNodes/index.tsx @@ -9,7 +9,7 @@ import ReactFlow, { Controls, Node, Edge, - OnLoadParams, + ReactFlowInstance, Connection, } from 'react-flow-renderer'; @@ -84,14 +84,14 @@ const initialEdges: Edge[] = [ ]; const NestedFlow = () => { - const [rfInstance, setRfInstance] = useState(null); + const [rfInstance, setRfInstance] = useState(null); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); const onConnect = useCallback((connection: Connection) => { setEdges((eds) => addEdge(connection, eds)); }, []); - const onLoad = useCallback((reactFlowInstance: OnLoadParams) => setRfInstance(reactFlowInstance), []); + const onPaneReady = useCallback((reactFlowInstance: ReactFlowInstance) => setRfInstance(reactFlowInstance), []); const updatePos = () => { setNodes((nds) => { @@ -131,7 +131,7 @@ const NestedFlow = () => { { +const onPaneReady = (reactFlowInstance: ReactFlowInstance) => { reactFlowInstance.fitView(); console.log(reactFlowInstance.getNodes()); }; @@ -55,7 +55,13 @@ const StressFlow = () => { }, []); return ( - + diff --git a/example/src/UpdatableEdge/index.tsx b/example/src/UpdatableEdge/index.tsx index 2b814472..eb34f063 100644 --- a/example/src/UpdatableEdge/index.tsx +++ b/example/src/UpdatableEdge/index.tsx @@ -5,7 +5,7 @@ import ReactFlow, { addEdge, applyNodeChanges, applyEdgeChanges, - OnLoadParams, + ReactFlowInstance, Connection, Edge, Node, @@ -53,7 +53,7 @@ const initialNodes: Node[] = [ const initialEdges = [{ id: 'e1-2', source: '1', target: '2', label: 'This is a draggable edge' }]; -const onLoad = (reactFlowInstance: OnLoadParams) => reactFlowInstance.fitView(); +const onPaneReady = (reactFlowInstance: ReactFlowInstance) => reactFlowInstance.fitView(); const onEdgeUpdateStart = (_: React.MouseEvent, edge: Edge) => console.log('start update', edge); const onEdgeUpdateEnd = (_: MouseEvent, edge: Edge) => console.log('end update', edge); @@ -79,7 +79,7 @@ const UpdatableEdge = () => { edges={edges} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} - onLoad={onLoad} + onPaneReady={onPaneReady} snapToGrid={true} onEdgeUpdate={onEdgeUpdate} onConnect={onConnect} diff --git a/src/components/StoreUpdater/index.tsx b/src/components/StoreUpdater/index.tsx index aa986471..4943aacd 100644 --- a/src/components/StoreUpdater/index.tsx +++ b/src/components/StoreUpdater/index.tsx @@ -36,6 +36,7 @@ interface StoreUpdaterProps { snapToGrid?: boolean; snapGrid?: SnapGrid; translateExtent?: CoordinateExtent; + fitViewOnInit: boolean; } const selector = (s: ReactFlowState) => ({ @@ -58,6 +59,7 @@ const selector = (s: ReactFlowState) => ({ setOnNodesChange: s.setOnNodesChange, setOnEdgesChange: s.setOnEdgesChange, reset: s.reset, + setFitViewOnInit: s.setFitViewOnInit, }); const StoreUpdater = ({ @@ -79,6 +81,7 @@ const StoreUpdater = ({ snapGrid, snapToGrid, translateExtent, + fitViewOnInit, }: StoreUpdaterProps) => { const { setNodes, @@ -100,6 +103,7 @@ const StoreUpdater = ({ setOnEdgesChange, setConnectionMode, reset, + setFitViewOnInit, } = useStore(selector, shallow); useEffect(() => { @@ -212,6 +216,10 @@ const StoreUpdater = ({ } }, [onEdgesChange]); + useEffect(() => { + setFitViewOnInit(fitViewOnInit); + }, [fitViewOnInit]); + return null; }; diff --git a/src/components/ViewFitter/index.tsx b/src/components/ViewFitter/index.tsx deleted file mode 100644 index f92733d2..00000000 --- a/src/components/ViewFitter/index.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React, { MutableRefObject, useEffect, useRef } from 'react'; - -import { Node } from '../../types'; -import { getRectOfNodes, getTransformForBounds } from '../../utils/graph'; -import { useStoreApi } from '../../store'; -import useZoomPanHelper from '../../hooks/useZoomPanHelper'; - -type ViewFitterInnerProps = { - nodes: Node[]; - viewFitted: MutableRefObject; -}; - -function ViewFitterInner({ nodes, viewFitted }: ViewFitterInnerProps) { - const store = useStoreApi(); - const { setTransform } = useZoomPanHelper(); - - useEffect(() => { - if (nodes.length > 0 && !viewFitted.current) { - const nodesInitialized = nodes.every((n) => n.width && n.height); - - if (nodesInitialized) { - const { width, height, minZoom, maxZoom } = store.getState(); - const bounds = getRectOfNodes(nodes); - const [x, y, zoom] = getTransformForBounds(bounds, width, height, minZoom ?? 0.5, maxZoom ?? 2); - - viewFitted.current = true; - setTransform({ x, y, zoom }); - } - } - }, [nodes]); - - return null; -} - -export type ViewFitterProps = { - nodes: Node[]; -}; - -function ViewFitter({ nodes }: ViewFitterProps) { - // we are storing the viewFitted in this wrapper to get rid of the useEffect of the inner component - // that needs to check if the nodes changed after we fitted the view. - const viewFitted = useRef(false); - - if (viewFitted.current) { - return null; - } - - return ; -} - -export default ViewFitter; diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index 5561833d..9a052bd0 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -5,7 +5,7 @@ import NodeRenderer from '../NodeRenderer'; import EdgeRenderer from '../EdgeRenderer'; import Viewport from '../Viewport'; import { ReactFlowProps } from '../ReactFlow'; -import useOnLoadHandler from '../../hooks/useOnLoadHandler'; +import useOnPaneReadyHandler from '../../hooks/useOnPaneReadyHandler'; import { NodeTypesType, EdgeTypesType, ConnectionLineType, KeyCode } from '../../types'; export interface GraphViewProps extends Omit { @@ -31,7 +31,7 @@ const GraphView = ({ onMove, onMoveStart, onMoveEnd, - onLoad, + onPaneReady, onNodeClick, onEdgeClick, onNodeDoubleClick, @@ -83,7 +83,7 @@ const GraphView = ({ noWheelClassName, noPanClassName, }: GraphViewProps) => { - useOnLoadHandler(onLoad); + useOnPaneReadyHandler(onPaneReady); return ( , 'onLoad'> { +export interface ReactFlowProps extends Omit, 'onPaneReady'> { nodes: Node[]; edges: Edge[]; onNodesChange?: (nodeChanges: NodeChange[]) => void; @@ -78,7 +77,7 @@ export interface ReactFlowProps extends Omit, 'on onConnectStart?: OnConnectStart; onConnectStop?: OnConnectStop; onConnectEnd?: OnConnectEnd; - onLoad?: OnLoad; + onPaneReady?: OnPaneReady; onMove?: (flowTransform?: FlowTransform) => void; onMoveStart?: (flowTransform?: FlowTransform) => void; onMoveEnd?: (flowTransform?: FlowTransform) => void; @@ -152,7 +151,7 @@ const ReactFlow: FunctionComponent = forwardRef = forwardRef = forwardRef - {fitViewOnInit && } {onSelectionChange && } {children} diff --git a/src/hooks/useOnLoadHandler.ts b/src/hooks/useOnPaneReadyHandler.ts similarity index 84% rename from src/hooks/useOnLoadHandler.ts rename to src/hooks/useOnPaneReadyHandler.ts index 4c50dc94..a51b6cdd 100644 --- a/src/hooks/useOnLoadHandler.ts +++ b/src/hooks/useOnPaneReadyHandler.ts @@ -2,10 +2,10 @@ import { useEffect, useRef } from 'react'; import { pointToRendererPoint } from '../utils/graph'; import { useStoreApi } from '../store'; -import useZoomPanHelper from '../hooks/useZoomPanHelper'; -import { OnLoad, XYPosition, Node, Edge, FlowExportObject } from '../types'; +import useZoomPanHelper from './useZoomPanHelper'; +import { OnPaneReady, XYPosition, Node, Edge, FlowExportObject } from '../types'; -function useOnLoadHandler(onLoad: OnLoad | undefined) { +function useOnPaneReadyHandler(onPaneReady: OnPaneReady | undefined) { const isInitialized = useRef(false); const store = useStoreApi(); const { zoomIn, zoomOut, zoomTo, getZoom, setTransform, getTransform, setCenter, fitView, initialized } = @@ -13,7 +13,7 @@ function useOnLoadHandler(onLoad: OnLoad | undefined) { useEffect(() => { if (!isInitialized.current && initialized) { - if (onLoad) { + if (onPaneReady) { const project = (position: XYPosition): XYPosition => { const { transform, snapToGrid, snapGrid } = store.getState(); return pointToRendererPoint(position, transform, snapToGrid, snapGrid); @@ -43,7 +43,7 @@ function useOnLoadHandler(onLoad: OnLoad | undefined) { }; }; - onLoad({ + onPaneReady({ fitView: (params = { padding: 0.1 }) => fitView(params), zoomIn, zoomOut, @@ -61,7 +61,7 @@ function useOnLoadHandler(onLoad: OnLoad | undefined) { isInitialized.current = true; } - }, [onLoad, zoomIn, zoomOut, zoomTo, setTransform, fitView, initialized]); + }, [onPaneReady, zoomIn, zoomOut, zoomTo, setTransform, fitView, initialized]); } -export default useOnLoadHandler; +export default useOnPaneReadyHandler; diff --git a/src/store/index.ts b/src/store/index.ts index d346d330..7f39354e 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -28,7 +28,7 @@ import { } from '../types'; import { getHandleBounds } from '../components/Nodes/utils'; import { createSelectionChange, getSelectionChanges } from '../utils/changes'; -import { createNodeInternals, createPositionChange, isParentSelected } from './utils'; +import { createNodeInternals, createPositionChange, fitView, isParentSelected } from './utils'; import initialState from './initialState'; const { Provider, useStore, useStoreApi } = createContext(); @@ -46,7 +46,7 @@ const createStore = () => set({ edges }); }, updateNodeDimensions: (updates: NodeDimensionUpdate[]) => { - const { onNodesChange, transform, nodeInternals } = get(); + const { onNodesChange, transform, nodeInternals, fitViewOnInit } = get(); const changes: NodeChange[] = updates.reduce((res, update) => { const node = nodeInternals.get(update.id); @@ -78,7 +78,9 @@ const createStore = () => return res; }, []); - set({ nodeInternals: new Map(nodeInternals) }); + const fitViewOnInitDone = fitViewOnInit && fitView(get); + + set({ nodeInternals: new Map(nodeInternals), fitViewOnInitDone }); if (changes?.length > 0) { onNodesChange?.(changes); @@ -234,6 +236,7 @@ const createStore = () => setOnNodesChange: (onNodesChange: OnNodesChange) => set({ onNodesChange }), setOnEdgesChange: (onEdgesChange: OnEdgesChange) => set({ onEdgesChange }), reset: () => set({ ...initialState }), + setFitViewOnInit: (fitViewOnInit: boolean) => set({ fitViewOnInit }), })); export { Provider, useStore, createStore, useStoreApi }; diff --git a/src/store/initialState.ts b/src/store/initialState.ts index 9da2acfe..98ea8c50 100644 --- a/src/store/initialState.ts +++ b/src/store/initialState.ts @@ -39,6 +39,9 @@ const initialState: ReactFlowStore = { multiSelectionActive: false, reactFlowVersion: typeof __REACT_FLOW_VERSION__ !== 'undefined' ? __REACT_FLOW_VERSION__ : '-', + + fitViewOnInit: false, + fitViewOnInitDone: false, }; export default initialState; diff --git a/src/store/utils.ts b/src/store/utils.ts index afb3e063..356261d4 100644 --- a/src/store/utils.ts +++ b/src/store/utils.ts @@ -1,13 +1,17 @@ +import { zoomIdentity } from 'd3-zoom'; +import { GetState } from 'zustand'; import { CoordinateExtent, Node, NodeDimensionChange, NodeInternals, NodeInternalsItem, + ReactFlowState, XYPosition, XYZPosition, } from '../types'; import { clampPosition, isNumeric } from '../utils'; +import { getRectOfNodes, getTransformForBounds } from '../utils/graph'; type ParentNodes = Record; @@ -172,3 +176,25 @@ export function createPositionChange({ return change; } + +export function fitView(get: GetState) { + let { nodeInternals, width, height, minZoom, maxZoom, d3Zoom, d3Selection, fitViewOnInitDone, fitViewOnInit } = get(); + + if (fitViewOnInit && !fitViewOnInitDone && d3Zoom && d3Selection) { + const rootNodes = Array.from(nodeInternals) + .filter(([_, n]) => !n.parentNode) + .map(([_, n]) => n); + const nodesInitialized = rootNodes.every((n) => n.width && n.height); + + if (rootNodes.length > 0 && nodesInitialized) { + const bounds = getRectOfNodes(rootNodes); + const [x, y, zoom] = getTransformForBounds(bounds, width, height, minZoom ?? 0.5, maxZoom ?? 2); + + const nextTransform = zoomIdentity.translate(x, y).scale(zoom); + d3Zoom.transform(d3Selection, nextTransform); + fitViewOnInitDone = true; + } + } + + return fitViewOnInitDone; +} diff --git a/src/types/general.ts b/src/types/general.ts index 9a75faf3..d06cb632 100644 --- a/src/types/general.ts +++ b/src/types/general.ts @@ -29,7 +29,7 @@ export type ZoomTo = (zoomLevel: number, options?: ZoomPanHelperFunctionOptions) export type SetTransform = (transform: FlowTransform, options?: ZoomPanHelperFunctionOptions) => void; export type SetCenter = (x: number, y: number, options?: SetCenterOptions) => void; -export type OnLoadParams = { +export type ReactFlowInstance = { zoomIn: ZoomInOut; zoomOut: ZoomInOut; zoomTo: ZoomTo; @@ -44,7 +44,7 @@ export type OnLoadParams = { toObject: ToObject; }; -export type OnLoad = (params: OnLoadParams) => void; +export type OnPaneReady = (reactFlowInstance: ReactFlowInstance) => void; export interface Connection { source: string | null; @@ -183,6 +183,9 @@ export type ReactFlowStore = { multiSelectionActive: boolean; reactFlowVersion: string; + + fitViewOnInit: boolean; + fitViewOnInitDone: boolean; }; export type ReactFlowActions = { @@ -226,6 +229,7 @@ export type ReactFlowActions = { onConnectEnd?: OnConnectEnd; reset: () => void; + setFitViewOnInit: (fitViewOnInit: boolean) => void; }; export type ReactFlowState = ReactFlowStore & ReactFlowActions; diff --git a/src/types/nodes.ts b/src/types/nodes.ts index d1e25896..08a3c463 100644 --- a/src/types/nodes.ts +++ b/src/types/nodes.ts @@ -8,8 +8,8 @@ import { HandleElement } from './handles'; export interface Node { id: string; position: XYPosition; + data: T; type?: string; - data?: T; style?: CSSProperties; className?: string; targetPosition?: Position;