fix(react): node observation #4064 (#4182)

* fix(react): node observation #4064

* chore(react): cleanup useNodeObserver

* chore(react): cleanup useNodeObserver
This commit is contained in:
Moritz Klack
2024-04-18 18:00:49 +02:00
committed by GitHub
parent 4a7ba3f6ea
commit 973a3ddd35
4 changed files with 94 additions and 77 deletions

View File

@@ -185,6 +185,7 @@ const CustomNodeFlow = () => {
maxZoom={5}
snapToGrid={snapToGrid}
fitView
onlyRenderVisibleElements
>
<Controls />
<Panel position="bottom-right">

View File

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

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

View File

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