refactor(general): fit view in store to prevent flickering, rename onLoad => onPaneReady

This commit is contained in:
moklick
2021-12-09 07:24:45 +01:00
parent 3a52efbb1f
commit 88582a86cc
17 changed files with 96 additions and 100 deletions
+5 -4
View File
@@ -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<OnLoadParams | null>(null);
const [rfInstance, setRfInstance] = useState<ReactFlowInstance | null>(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 = () => {
<ReactFlow
nodes={nodes}
edges={edges}
onLoad={onLoad}
onPaneReady={onPaneReady}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
@@ -167,6 +167,7 @@ const BasicFlow = () => {
maxZoom={4}
onlyRenderVisibleElements={false}
nodeTypes={nodeTypes}
fitViewOnInit
>
<MiniMap />
<Controls />
+5 -4
View File
@@ -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
>
<MiniMap
nodeStrokeColor={(n: Node): string => {
@@ -137,4 +138,4 @@ const CustomNodeFlow = () => {
);
};
export default CustomNodeFlow;
export default CustomNodeFlow;
+3 -3
View File
@@ -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}
>
+1 -5
View File
@@ -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}
>
+4 -4
View File
@@ -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<OnLoadParams | null>(null);
const [rfInstance, setRfInstance] = useState<ReactFlowInstance | null>(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 = () => {
<ReactFlow
nodes={nodes}
edges={edges}
onLoad={onLoad}
onPaneReady={onPaneReady}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
+9 -3
View File
@@ -3,7 +3,7 @@ import ReactFlow, {
MiniMap,
Controls,
Background,
OnLoadParams,
ReactFlowInstance,
Edge,
Node,
NodeChange,
@@ -16,7 +16,7 @@ import { getNodesAndEdges } from './utils';
const buttonWrapperStyles: CSSProperties = { position: 'absolute', right: 10, top: 10, zIndex: 4 };
const onLoad = (reactFlowInstance: OnLoadParams) => {
const onPaneReady = (reactFlowInstance: ReactFlowInstance) => {
reactFlowInstance.fitView();
console.log(reactFlowInstance.getNodes());
};
@@ -55,7 +55,13 @@ const StressFlow = () => {
}, []);
return (
<ReactFlow nodes={nodes} edges={edges} onLoad={onLoad} onConnect={onConnect} onNodesChange={onNodesChange}>
<ReactFlow
nodes={nodes}
edges={edges}
onPaneReady={onPaneReady}
onConnect={onConnect}
onNodesChange={onNodesChange}
>
<MiniMap />
<Controls />
<Background />
+3 -3
View File
@@ -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}
+8
View File
@@ -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;
};
-51
View File
@@ -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<boolean>;
};
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<boolean>(false);
if (viewFitted.current) {
return null;
}
return <ViewFitterInner nodes={nodes} viewFitted={viewFitted} />;
}
export default ViewFitter;
+3 -3
View File
@@ -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<ReactFlowProps, 'onSelectionChange' | 'nodes' | 'edges'> {
@@ -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 (
<FlowRenderer
+6 -7
View File
@@ -19,12 +19,10 @@ import SelectionListener from '../../components/SelectionListener';
import { BezierEdge, StepEdge, SmoothStepEdge, StraightEdge } from '../../components/Edges';
import { createEdgeTypes } from '../EdgeRenderer/utils';
import Wrapper from './Wrapper';
import ViewFitter from '../../components/ViewFitter';
import {
OnSelectionChangeFunc,
NodeTypesType,
EdgeTypesType,
OnLoad,
Node,
Edge,
ConnectionMode,
@@ -41,6 +39,7 @@ import {
OnEdgeUpdateFunc,
NodeChange,
EdgeChange,
OnPaneReady,
} from '../../types';
import '../../style.css';
@@ -59,7 +58,7 @@ const defaultEdgeTypes = {
smoothstep: SmoothStepEdge,
};
export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onLoad'> {
export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onPaneReady'> {
nodes: Node[];
edges: Edge[];
onNodesChange?: (nodeChanges: NodeChange[]) => void;
@@ -78,7 +77,7 @@ export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, '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<ReactFlowProps> = forwardRef<ReactFlowRefType
edgeTypes = defaultEdgeTypes,
onNodeClick,
onEdgeClick,
onLoad,
onPaneReady,
onMove,
onMoveStart,
onMoveEnd,
@@ -234,7 +233,7 @@ const ReactFlow: FunctionComponent<ReactFlowProps> = forwardRef<ReactFlowRefType
<div {...rest} ref={ref} className={reactFlowClasses}>
<Wrapper>
<GraphView
onLoad={onLoad}
onPaneReady={onPaneReady}
onMove={onMove}
onMoveStart={onMoveStart}
onMoveEnd={onMoveEnd}
@@ -309,8 +308,8 @@ const ReactFlow: FunctionComponent<ReactFlowProps> = forwardRef<ReactFlowRefType
snapGrid={snapGrid}
connectionMode={connectionMode}
translateExtent={translateExtent}
fitViewOnInit={fitViewOnInit}
/>
{fitViewOnInit && <ViewFitter nodes={nodes} />}
{onSelectionChange && <SelectionListener onSelectionChange={onSelectionChange} />}
{children}
</Wrapper>
@@ -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<any> | undefined) {
function useOnPaneReadyHandler(onPaneReady: OnPaneReady<any> | undefined) {
const isInitialized = useRef<boolean>(false);
const store = useStoreApi();
const { zoomIn, zoomOut, zoomTo, getZoom, setTransform, getTransform, setCenter, fitView, initialized } =
@@ -13,7 +13,7 @@ function useOnLoadHandler(onLoad: OnLoad<any> | 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<any> | undefined) {
};
};
onLoad({
onPaneReady({
fitView: (params = { padding: 0.1 }) => fitView(params),
zoomIn,
zoomOut,
@@ -61,7 +61,7 @@ function useOnLoadHandler(onLoad: OnLoad<any> | undefined) {
isInitialized.current = true;
}
}, [onLoad, zoomIn, zoomOut, zoomTo, setTransform, fitView, initialized]);
}, [onPaneReady, zoomIn, zoomOut, zoomTo, setTransform, fitView, initialized]);
}
export default useOnLoadHandler;
export default useOnPaneReadyHandler;
+6 -3
View File
@@ -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<ReactFlowState>();
@@ -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<NodeChange[]>((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 };
+3
View File
@@ -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;
+26
View File
@@ -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<string, boolean>;
@@ -172,3 +176,25 @@ export function createPositionChange({
return change;
}
export function fitView(get: GetState<ReactFlowState>) {
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;
}
+6 -2
View File
@@ -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<T = any> = {
export type ReactFlowInstance<T = any> = {
zoomIn: ZoomInOut;
zoomOut: ZoomInOut;
zoomTo: ZoomTo;
@@ -44,7 +44,7 @@ export type OnLoadParams<T = any> = {
toObject: ToObject<T>;
};
export type OnLoad<T = any> = (params: OnLoadParams<T>) => void;
export type OnPaneReady<T = any> = (reactFlowInstance: ReactFlowInstance<T>) => 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;
+1 -1
View File
@@ -8,8 +8,8 @@ import { HandleElement } from './handles';
export interface Node<T = any> {
id: string;
position: XYPosition;
data: T;
type?: string;
data?: T;
style?: CSSProperties;
className?: string;
targetPosition?: Position;