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;
}