* fix(react): node observation #4064 * chore(react): cleanup useNodeObserver * chore(react): cleanup useNodeObserver
This commit is contained in:
@@ -185,6 +185,7 @@ const CustomNodeFlow = () => {
|
||||
maxZoom={5}
|
||||
snapToGrid={snapToGrid}
|
||||
fitView
|
||||
onlyRenderVisibleElements
|
||||
>
|
||||
<Controls />
|
||||
<Panel position="bottom-right">
|
||||
|
||||
@@ -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<NodeType extends Node>({
|
||||
@@ -68,58 +69,8 @@ export function NodeWrapper<NodeType extends Node>({
|
||||
const isFocusable = !!(node.focusable || (nodesFocusable && typeof node.focusable === 'undefined'));
|
||||
|
||||
const store = useStoreApi();
|
||||
const nodeRef = useRef<HTMLDivElement>(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<NodeType extends Node>({
|
||||
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<NodeType extends Node>({
|
||||
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<NodeType extends Node>({
|
||||
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<NodeType extends Node>({
|
||||
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}
|
||||
|
||||
71
packages/react/src/components/NodeWrapper/useNodeObserver.ts
Normal file
71
packages/react/src/components/NodeWrapper/useNodeObserver.ts
Normal file
@@ -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<HTMLDivElement | null>(null);
|
||||
const observedNode = useRef<HTMLDivElement | null>(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;
|
||||
}
|
||||
@@ -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<ResizeObserver>();
|
||||
|
||||
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<string, InternalNodeUpdate>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user