diff --git a/examples/react/src/examples/NodeResizer/index.tsx b/examples/react/src/examples/NodeResizer/index.tsx index 7afb411f..8d5b7602 100644 --- a/examples/react/src/examples/NodeResizer/index.tsx +++ b/examples/react/src/examples/NodeResizer/index.tsx @@ -185,6 +185,7 @@ const CustomNodeFlow = () => { maxZoom={5} snapToGrid={snapToGrid} fitView + onlyRenderVisibleElements > diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index acc0d7fe..dfbb7b93 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, type MouseEvent, type KeyboardEvent } from 'react'; +import { type MouseEvent, type KeyboardEvent } from 'react'; import cc from 'classcat'; import { shallow } from 'zustand/shallow'; import { @@ -18,6 +18,7 @@ import { useDrag } from '../../hooks/useDrag'; import { useMoveSelectedNodes } from '../../hooks/useMoveSelectedNodes'; import { handleNodeClick } from '../Nodes/utils'; import { arrowKeyDiffs, builtinNodeTypes, getNodeInlineStyleDimensions } from './utils'; +import { useNodeObserver } from './useNodeObserver'; import type { InternalNode, Node, NodeWrapperProps } from '../../types'; export function NodeWrapper({ @@ -68,58 +69,8 @@ export function NodeWrapper({ const isFocusable = !!(node.focusable || (nodesFocusable && typeof node.focusable === 'undefined')); const store = useStoreApi(); - const nodeRef = useRef(null); - const prevSourcePosition = useRef(node.sourcePosition); - const prevTargetPosition = useRef(node.targetPosition); - const prevType = useRef(nodeType); - - const nodeDimensions = getNodeDimensions(node); - const inlineDimensions = getNodeInlineStyleDimensions(node); - const initialized = nodeHasDimensions(node); - const hasHandleBounds = !!node.internals.handleBounds; - - const moveSelectedNodes = useMoveSelectedNodes(); - - useEffect(() => { - const currNode = nodeRef.current; - - return () => { - if (currNode) { - resizeObserver?.unobserve(currNode); - } - }; - }, []); - - useEffect(() => { - if (nodeRef.current && !node.hidden) { - const currNode = nodeRef.current; - if (!initialized || !hasHandleBounds) { - resizeObserver?.unobserve(currNode); - resizeObserver?.observe(currNode); - } - } - }, [node.hidden, initialized, hasHandleBounds]); - - useEffect(() => { - // when the user programmatically changes the source or handle position, we re-initialize the node - const typeChanged = prevType.current !== nodeType; - const sourcePosChanged = prevSourcePosition.current !== node.sourcePosition; - const targetPosChanged = prevTargetPosition.current !== node.targetPosition; - - if (nodeRef.current && (typeChanged || sourcePosChanged || targetPosChanged)) { - if (typeChanged) { - prevType.current = nodeType; - } - if (sourcePosChanged) { - prevSourcePosition.current = node.sourcePosition; - } - if (targetPosChanged) { - prevTargetPosition.current = node.targetPosition; - } - store.getState().updateNodeInternals(new Map([[id, { id, nodeElement: nodeRef.current, force: true }]])); - } - }, [id, nodeType, node.sourcePosition, node.targetPosition]); - + const hasDimensions = nodeHasDimensions(node); + const nodeRef = useNodeObserver({ node, nodeType, hasDimensions, resizeObserver }); const dragging = useDrag({ nodeRef, disabled: node.hidden || !isDraggable, @@ -128,20 +79,21 @@ export function NodeWrapper({ nodeId: id, isSelectable, }); + const moveSelectedNodes = useMoveSelectedNodes(); if (node.hidden) { return null; } - const positionAbsolute = nodeExtent - ? clampPosition(node.internals.positionAbsolute, nodeExtent) - : node.internals.positionAbsolute || { x: 0, y: 0 }; + const nodeDimensions = getNodeDimensions(node); + const inlineDimensions = getNodeInlineStyleDimensions(node); + const clampedPosition = nodeExtent + ? clampPosition(internals.positionAbsolute, nodeExtent) + : internals.positionAbsolute; const positionWithOrigin = getPositionWithOrigin({ - x: positionAbsolute.x, - y: positionAbsolute.y, - width: nodeDimensions.width, - height: nodeDimensions.height, + ...clampedPosition, + ...nodeDimensions, origin: node.origin || nodeOrigin, }); const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave; @@ -188,7 +140,7 @@ export function NodeWrapper({ store.setState({ ariaLiveMessage: `Moved selected node ${event.key .replace('Arrow', '') - .toLowerCase()}. New position, x: ${~~positionAbsolute.x}, y: ${~~positionAbsolute.y}`, + .toLowerCase()}. New position, x: ${~~clampedPosition.x}, y: ${~~clampedPosition.y}`, }); moveSelectedNodes({ @@ -221,7 +173,7 @@ export function NodeWrapper({ zIndex: internals.z, transform: `translate(${positionWithOrigin.x}px,${positionWithOrigin.y}px)`, pointerEvents: hasPointerEvents ? 'all' : 'none', - visibility: initialized ? 'visible' : 'hidden', + visibility: hasDimensions ? 'visible' : 'hidden', ...node.style, ...inlineDimensions, }} @@ -244,8 +196,8 @@ export function NodeWrapper({ id={id} data={node.data} type={nodeType} - positionAbsoluteX={positionAbsolute.x} - positionAbsoluteY={positionAbsolute.y} + positionAbsoluteX={clampedPosition.x} + positionAbsoluteY={clampedPosition.y} selected={node.selected} isConnectable={isConnectable} sourcePosition={node.sourcePosition} diff --git a/packages/react/src/components/NodeWrapper/useNodeObserver.ts b/packages/react/src/components/NodeWrapper/useNodeObserver.ts new file mode 100644 index 00000000..1476e920 --- /dev/null +++ b/packages/react/src/components/NodeWrapper/useNodeObserver.ts @@ -0,0 +1,71 @@ +import { useEffect, useRef } from 'react'; + +import type { InternalNode } from '../../types'; +import { useStoreApi } from '../../hooks/useStore'; + +/** + * Hook to handle the resize observation + internal updates for the passed node. + * + * @internal + * @returns nodeRef - reference to the node element + */ +export function useNodeObserver({ + node, + nodeType, + hasDimensions, + resizeObserver, +}: { + node: InternalNode; + nodeType: string; + hasDimensions: boolean; + resizeObserver: ResizeObserver | null; +}) { + const store = useStoreApi(); + const nodeRef = useRef(null); + const observedNode = useRef(null); + const prevSourcePosition = useRef(node.sourcePosition); + const prevTargetPosition = useRef(node.targetPosition); + const prevType = useRef(nodeType); + const isInitialized = hasDimensions && !!node.internals.handleBounds && !node.hidden; + + useEffect(() => { + if (nodeRef.current && (!isInitialized || observedNode.current !== nodeRef.current)) { + if (observedNode.current) { + resizeObserver?.unobserve(observedNode.current); + } + resizeObserver?.observe(nodeRef.current); + observedNode.current = nodeRef.current; + } + }, [isInitialized]); + + useEffect(() => { + return () => { + if (observedNode.current) { + resizeObserver?.unobserve(observedNode.current); + observedNode.current = null; + } + }; + }, []); + + useEffect(() => { + if (nodeRef.current) { + // when the user programmatically changes the source or handle position, we need to update the internals + // to make sure the edges are updated correctly + const typeChanged = prevType.current !== nodeType; + const sourcePosChanged = prevSourcePosition.current !== node.sourcePosition; + const targetPosChanged = prevTargetPosition.current !== node.targetPosition; + + if (typeChanged || sourcePosChanged || targetPosChanged) { + prevType.current = nodeType; + prevSourcePosition.current = node.sourcePosition; + prevTargetPosition.current = node.targetPosition; + + store + .getState() + .updateNodeInternals(new Map([[node.id, { id: node.id, nodeElement: nodeRef.current, force: true }]])); + } + } + }, [node.id, nodeType, node.sourcePosition, node.targetPosition]); + + return nodeRef; +} diff --git a/packages/react/src/container/NodeRenderer/useResizeObserver.ts b/packages/react/src/container/NodeRenderer/useResizeObserver.ts index fa7d501f..b722d440 100644 --- a/packages/react/src/container/NodeRenderer/useResizeObserver.ts +++ b/packages/react/src/container/NodeRenderer/useResizeObserver.ts @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef } from 'react'; +import { useEffect, useState } from 'react'; import { ReactFlowState } from '../../types'; import { useStore } from '../../hooks/useStore'; @@ -8,16 +8,13 @@ const selector = (s: ReactFlowState) => s.updateNodeInternals; export function useResizeObserver() { const updateNodeInternals = useStore(selector); - const resizeObserverRef = useRef(); - - const resizeObserver = useMemo(() => { + const [resizeObserver] = useState(() => { if (typeof ResizeObserver === 'undefined') { return null; } - const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => { + return new ResizeObserver((entries: ResizeObserverEntry[]) => { const updates = new Map(); - entries.forEach((entry: ResizeObserverEntry) => { const id = entry.target.getAttribute('data-id') as string; updates.set(id, { @@ -28,17 +25,13 @@ export function useResizeObserver() { updateNodeInternals(updates); }); - - resizeObserverRef.current = observer; - - return observer; - }, []); + }); useEffect(() => { return () => { - resizeObserverRef?.current?.disconnect(); + resizeObserver?.disconnect(); }; - }, []); + }, [resizeObserver]); return resizeObserver; }