diff --git a/examples/react/src/examples/CustomMiniMapNode/index.tsx b/examples/react/src/examples/CustomMiniMapNode/index.tsx index 0b62196f..7ecf1c54 100644 --- a/examples/react/src/examples/CustomMiniMapNode/index.tsx +++ b/examples/react/src/examples/CustomMiniMapNode/index.tsx @@ -27,9 +27,9 @@ const buttonStyle: CSSProperties = { zIndex: 4, }; -const CustomMiniMapNode = ({ x, y, width, height, color }: MiniMapNodeProps) => ( - -); +const CustomMiniMapNode = ({ x, y, width, height }: MiniMapNodeProps) => { + return ; +}; const CustomMiniMapNodeFlow = () => { const [nodes, setNodes, onNodesChange] = useNodesState([]); diff --git a/examples/react/src/examples/Subflow/index.tsx b/examples/react/src/examples/Subflow/index.tsx index 19dc119a..8ea28e34 100644 --- a/examples/react/src/examples/Subflow/index.tsx +++ b/examples/react/src/examples/Subflow/index.tsx @@ -90,7 +90,7 @@ const initialNodes: Node[] = [ data: { label: 'Node 5' }, position: { x: 650, y: 250 }, className: 'light', - style: { width: 400, height: 150 }, + style: { width: 100, height: 100 }, zIndex: 1000, }, { @@ -161,9 +161,12 @@ const Subflow = () => { setNodes((nds) => { return nds.map((n) => { if (!n.parentNode) { - n.position = { - x: Math.random() * 400, - y: Math.random() * 400, + return { + ...n, + position: { + x: Math.random() * 400, + y: Math.random() * 400, + }, }; } @@ -178,8 +181,10 @@ const Subflow = () => { const toggleClassnames = () => { setNodes((nds) => { return nds.map((n) => { - n.className = n.className === 'light' ? 'dark' : 'light'; - return n; + return { + ...n, + className: n.className === 'light' ? 'dark' : 'light', + }; }); }); }; @@ -187,8 +192,10 @@ const Subflow = () => { const toggleChildNodes = () => { setNodes((nds) => { return nds.map((n) => { - n.hidden = !!n.parentNode && !n.hidden; - return n; + return { + ...n, + hidden: !!n.parentNode && !n.hidden, + }; }); }); }; @@ -215,19 +222,12 @@ const Subflow = () => { - - - - + + + + + ); diff --git a/packages/react/src/components/EdgeWrapper/index.tsx b/packages/react/src/components/EdgeWrapper/index.tsx index 28323a19..5973751d 100644 --- a/packages/react/src/components/EdgeWrapper/index.tsx +++ b/packages/react/src/components/EdgeWrapper/index.tsx @@ -101,12 +101,12 @@ export function EdgeWrapper({ ); const markerStartUrl = useMemo( - () => (edge.markerStart ? `url(#${getMarkerId(edge.markerStart, rfId)})` : undefined), + () => (edge.markerStart ? `url('#${getMarkerId(edge.markerStart, rfId)}')` : undefined), [edge.markerStart, rfId] ); const markerEndUrl = useMemo( - () => (edge.markerEnd ? `url(#${getMarkerId(edge.markerEnd, rfId)})` : undefined), + () => (edge.markerEnd ? `url('#${getMarkerId(edge.markerEnd, rfId)}')` : undefined), [edge.markerEnd, rfId] ); diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index ebf65163..548a4aa4 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -79,16 +79,33 @@ export function NodeWrapper({ const prevTargetPosition = useRef(node.targetPosition); const prevType = useRef(nodeType); + const width = node.width ?? undefined; + const height = node.height ?? undefined; + const computedWidth = node.computed?.width; + const computedHeight = node.computed?.height; + const initialized = (!!computedWidth && !!computedHeight) || (!!width && !!height); + const hasHandleBounds = !!node[internalsSymbol]?.handleBounds; + const moveSelectedNodes = useMoveSelectedNodes(); + useEffect(() => { + return () => { + if (nodeRef.current) { + resizeObserver?.unobserve(nodeRef.current); + } + }; + }, []); + useEffect(() => { if (nodeRef.current && !node.hidden) { const currNode = nodeRef.current; - resizeObserver?.observe(currNode); - return () => resizeObserver?.unobserve(currNode); + if (!initialized || !hasHandleBounds) { + resizeObserver?.unobserve(currNode); + resizeObserver?.observe(currNode); + } } - }, [node.hidden]); + }, [node.hidden, initialized, hasHandleBounds]); useEffect(() => { // when the user programmatically changes the source or handle position, we re-initialize the node @@ -123,11 +140,6 @@ export function NodeWrapper({ return null; } - const width = node.width ?? undefined; - const height = node.height ?? undefined; - const computedWidth = node.computed?.width; - const computedHeight = node.computed?.height; - const positionAbsoluteOrigin = getPositionWithOrigin({ x: positionAbsoluteX, y: positionAbsoluteY, @@ -135,7 +147,6 @@ export function NodeWrapper({ height: computedHeight ?? height ?? 0, origin: node.origin || nodeOrigin, }); - const initialized = (!!computedWidth && !!computedHeight) || (!!width && !!height); const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave; const onMouseEnterHandler = onMouseEnter ? (event: MouseEvent) => onMouseEnter(event, { ...node }) : undefined; diff --git a/packages/react/src/components/StoreUpdater/index.tsx b/packages/react/src/components/StoreUpdater/index.tsx index 96080368..f1d68e6c 100644 --- a/packages/react/src/components/StoreUpdater/index.tsx +++ b/packages/react/src/components/StoreUpdater/index.tsx @@ -9,7 +9,7 @@ import { infiniteExtent, type CoordinateExtent } from '@xyflow/system'; import { useStore, useStoreApi } from '../../hooks/useStore'; import type { Node, Edge, ReactFlowState, ReactFlowProps, FitViewOptions } from '../../types'; -import { initNodeOrigin } from '../../container/ReactFlow'; +import { defaultNodeOrigin } from '../../container/ReactFlow/init-values'; // these fields exist in the global store and we need to keep them up to date const reactFlowFieldsToTrack = [ @@ -84,6 +84,19 @@ const selector = (s: ReactFlowState) => ({ setDefaultNodesAndEdges: s.setDefaultNodesAndEdges, }); +const initPrevValues = { + // these are values that are also passed directly to other components + // than the StoreUpdater. We can reduce the number of setStore calls + // by setting the same values here as prev fields. + translateExtent: infiniteExtent, + nodeOrigin: defaultNodeOrigin, + minZoom: 0.5, + maxZoom: 2, + elementsSelectable: true, + noPanClassName: 'nopan', + rfId: '1', +}; + export function StoreUpdater(props: StoreUpdaterProps) { const { setNodes, @@ -99,23 +112,15 @@ export function StoreUpdater(props: StoreUpdaterProps) { useEffect(() => { setDefaultNodesAndEdges(props.defaultNodes, props.defaultEdges); + return () => { + // when we reset the store we also need to reset the previous fields + previousFields.current = initPrevValues; reset(); }; }, []); - const previousFields = useRef>({ - // these are values that are also passed directly to other components - // than the StoreUpdater. We can reduce the number of setStore calls - // by setting the same values here as prev fields. - translateExtent: infiniteExtent, - nodeOrigin: initNodeOrigin, - minZoom: 0.5, - maxZoom: 2, - elementsSelectable: true, - noPanClassName: 'nopan', - rfId: '1', - }); + const previousFields = useRef>(initPrevValues); useEffect( () => { diff --git a/packages/react/src/container/ReactFlow/index.tsx b/packages/react/src/container/ReactFlow/index.tsx index df9936a1..6448abe4 100644 --- a/packages/react/src/container/ReactFlow/index.tsx +++ b/packages/react/src/container/ReactFlow/index.tsx @@ -1,14 +1,6 @@ import { forwardRef, type CSSProperties } from 'react'; import cc from 'classcat'; -import { - ConnectionLineType, - PanOnScrollMode, - SelectionMode, - infiniteExtent, - isMacOs, - type NodeOrigin, - type Viewport, -} from '@xyflow/system'; +import { ConnectionLineType, PanOnScrollMode, SelectionMode, infiniteExtent, isMacOs } from '@xyflow/system'; import { A11yDescriptions } from '../../components/A11yDescriptions'; import { Attribution } from '../../components/Attribution'; @@ -18,9 +10,7 @@ import { useColorModeClass } from '../../hooks/useColorModeClass'; import { GraphView } from '../GraphView'; import { Wrapper } from './Wrapper'; import type { ReactFlowProps, ReactFlowRefType } from '../../types'; - -export const initNodeOrigin: NodeOrigin = [0, 0]; -const initDefaultViewport: Viewport = { x: 0, y: 0, zoom: 1 }; +import { defaultViewport as initViewport, defaultNodeOrigin } from './init-values'; const wrapperStyle: CSSProperties = { width: '100%', @@ -89,11 +79,11 @@ const ReactFlow = forwardRef( nodesDraggable, nodesConnectable, nodesFocusable, - nodeOrigin = initNodeOrigin, + nodeOrigin = defaultNodeOrigin, edgesFocusable, edgesUpdatable, elementsSelectable = true, - defaultViewport = initDefaultViewport, + defaultViewport = initViewport, minZoom = 0.5, maxZoom = 2, translateExtent = infiniteExtent, diff --git a/packages/react/src/container/ReactFlow/init-values.ts b/packages/react/src/container/ReactFlow/init-values.ts new file mode 100644 index 00000000..75e7fcaa --- /dev/null +++ b/packages/react/src/container/ReactFlow/init-values.ts @@ -0,0 +1,4 @@ +import { type NodeOrigin, Viewport } from '@xyflow/system'; + +export const defaultNodeOrigin: NodeOrigin = [0, 0]; +export const defaultViewport: Viewport = { x: 0, y: 0, zoom: 1 }; diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 46447e00..71a51a1e 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -303,13 +303,7 @@ const createRFStore = ({ set(currentConnection); }, - reset: () => { - // @todo: what should we do about this? Do we still need it? - // if you are on a SPA with multiple flows, we want to make sure that the store gets resetted - // when you switch pages. Does this reset solves this? Currently it always gets called. This - // leads to an emtpy nodes array at the beginning. - // set({ ...getInitialState() }); - }, + reset: () => set({ ...getInitialState() }), }), Object.is ); diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts index 5cb8a382..c04c7e51 100644 --- a/packages/system/src/utils/store.ts +++ b/packages/system/src/utils/store.ts @@ -94,8 +94,8 @@ export function adoptUserProvidedNodes( ...n, computed: { positionAbsolute: n.position, - width: n.computed?.width || currentStoreNode?.computed?.width, - height: n.computed?.height || currentStoreNode?.computed?.height, + width: n.computed?.width, + height: n.computed?.height, }, }; const z = (isNumeric(n.zIndex) ? n.zIndex : 0) + (n.selected ? selectedNodeZ : 0);