* 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}
|
maxZoom={5}
|
||||||
snapToGrid={snapToGrid}
|
snapToGrid={snapToGrid}
|
||||||
fitView
|
fitView
|
||||||
|
onlyRenderVisibleElements
|
||||||
>
|
>
|
||||||
<Controls />
|
<Controls />
|
||||||
<Panel position="bottom-right">
|
<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 cc from 'classcat';
|
||||||
import { shallow } from 'zustand/shallow';
|
import { shallow } from 'zustand/shallow';
|
||||||
import {
|
import {
|
||||||
@@ -18,6 +18,7 @@ import { useDrag } from '../../hooks/useDrag';
|
|||||||
import { useMoveSelectedNodes } from '../../hooks/useMoveSelectedNodes';
|
import { useMoveSelectedNodes } from '../../hooks/useMoveSelectedNodes';
|
||||||
import { handleNodeClick } from '../Nodes/utils';
|
import { handleNodeClick } from '../Nodes/utils';
|
||||||
import { arrowKeyDiffs, builtinNodeTypes, getNodeInlineStyleDimensions } from './utils';
|
import { arrowKeyDiffs, builtinNodeTypes, getNodeInlineStyleDimensions } from './utils';
|
||||||
|
import { useNodeObserver } from './useNodeObserver';
|
||||||
import type { InternalNode, Node, NodeWrapperProps } from '../../types';
|
import type { InternalNode, Node, NodeWrapperProps } from '../../types';
|
||||||
|
|
||||||
export function NodeWrapper<NodeType extends Node>({
|
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 isFocusable = !!(node.focusable || (nodesFocusable && typeof node.focusable === 'undefined'));
|
||||||
|
|
||||||
const store = useStoreApi();
|
const store = useStoreApi();
|
||||||
const nodeRef = useRef<HTMLDivElement>(null);
|
const hasDimensions = nodeHasDimensions(node);
|
||||||
const prevSourcePosition = useRef(node.sourcePosition);
|
const nodeRef = useNodeObserver({ node, nodeType, hasDimensions, resizeObserver });
|
||||||
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 dragging = useDrag({
|
const dragging = useDrag({
|
||||||
nodeRef,
|
nodeRef,
|
||||||
disabled: node.hidden || !isDraggable,
|
disabled: node.hidden || !isDraggable,
|
||||||
@@ -128,20 +79,21 @@ export function NodeWrapper<NodeType extends Node>({
|
|||||||
nodeId: id,
|
nodeId: id,
|
||||||
isSelectable,
|
isSelectable,
|
||||||
});
|
});
|
||||||
|
const moveSelectedNodes = useMoveSelectedNodes();
|
||||||
|
|
||||||
if (node.hidden) {
|
if (node.hidden) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const positionAbsolute = nodeExtent
|
const nodeDimensions = getNodeDimensions(node);
|
||||||
? clampPosition(node.internals.positionAbsolute, nodeExtent)
|
const inlineDimensions = getNodeInlineStyleDimensions(node);
|
||||||
: node.internals.positionAbsolute || { x: 0, y: 0 };
|
const clampedPosition = nodeExtent
|
||||||
|
? clampPosition(internals.positionAbsolute, nodeExtent)
|
||||||
|
: internals.positionAbsolute;
|
||||||
|
|
||||||
const positionWithOrigin = getPositionWithOrigin({
|
const positionWithOrigin = getPositionWithOrigin({
|
||||||
x: positionAbsolute.x,
|
...clampedPosition,
|
||||||
y: positionAbsolute.y,
|
...nodeDimensions,
|
||||||
width: nodeDimensions.width,
|
|
||||||
height: nodeDimensions.height,
|
|
||||||
origin: node.origin || nodeOrigin,
|
origin: node.origin || nodeOrigin,
|
||||||
});
|
});
|
||||||
const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave;
|
const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave;
|
||||||
@@ -188,7 +140,7 @@ export function NodeWrapper<NodeType extends Node>({
|
|||||||
store.setState({
|
store.setState({
|
||||||
ariaLiveMessage: `Moved selected node ${event.key
|
ariaLiveMessage: `Moved selected node ${event.key
|
||||||
.replace('Arrow', '')
|
.replace('Arrow', '')
|
||||||
.toLowerCase()}. New position, x: ${~~positionAbsolute.x}, y: ${~~positionAbsolute.y}`,
|
.toLowerCase()}. New position, x: ${~~clampedPosition.x}, y: ${~~clampedPosition.y}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
moveSelectedNodes({
|
moveSelectedNodes({
|
||||||
@@ -221,7 +173,7 @@ export function NodeWrapper<NodeType extends Node>({
|
|||||||
zIndex: internals.z,
|
zIndex: internals.z,
|
||||||
transform: `translate(${positionWithOrigin.x}px,${positionWithOrigin.y}px)`,
|
transform: `translate(${positionWithOrigin.x}px,${positionWithOrigin.y}px)`,
|
||||||
pointerEvents: hasPointerEvents ? 'all' : 'none',
|
pointerEvents: hasPointerEvents ? 'all' : 'none',
|
||||||
visibility: initialized ? 'visible' : 'hidden',
|
visibility: hasDimensions ? 'visible' : 'hidden',
|
||||||
...node.style,
|
...node.style,
|
||||||
...inlineDimensions,
|
...inlineDimensions,
|
||||||
}}
|
}}
|
||||||
@@ -244,8 +196,8 @@ export function NodeWrapper<NodeType extends Node>({
|
|||||||
id={id}
|
id={id}
|
||||||
data={node.data}
|
data={node.data}
|
||||||
type={nodeType}
|
type={nodeType}
|
||||||
positionAbsoluteX={positionAbsolute.x}
|
positionAbsoluteX={clampedPosition.x}
|
||||||
positionAbsoluteY={positionAbsolute.y}
|
positionAbsoluteY={clampedPosition.y}
|
||||||
selected={node.selected}
|
selected={node.selected}
|
||||||
isConnectable={isConnectable}
|
isConnectable={isConnectable}
|
||||||
sourcePosition={node.sourcePosition}
|
sourcePosition={node.sourcePosition}
|
||||||
|
|||||||
@@ -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 { ReactFlowState } from '../../types';
|
||||||
import { useStore } from '../../hooks/useStore';
|
import { useStore } from '../../hooks/useStore';
|
||||||
@@ -8,16 +8,13 @@ const selector = (s: ReactFlowState) => s.updateNodeInternals;
|
|||||||
|
|
||||||
export function useResizeObserver() {
|
export function useResizeObserver() {
|
||||||
const updateNodeInternals = useStore(selector);
|
const updateNodeInternals = useStore(selector);
|
||||||
const resizeObserverRef = useRef<ResizeObserver>();
|
const [resizeObserver] = useState(() => {
|
||||||
|
|
||||||
const resizeObserver = useMemo(() => {
|
|
||||||
if (typeof ResizeObserver === 'undefined') {
|
if (typeof ResizeObserver === 'undefined') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
|
return new ResizeObserver((entries: ResizeObserverEntry[]) => {
|
||||||
const updates = new Map<string, InternalNodeUpdate>();
|
const updates = new Map<string, InternalNodeUpdate>();
|
||||||
|
|
||||||
entries.forEach((entry: ResizeObserverEntry) => {
|
entries.forEach((entry: ResizeObserverEntry) => {
|
||||||
const id = entry.target.getAttribute('data-id') as string;
|
const id = entry.target.getAttribute('data-id') as string;
|
||||||
updates.set(id, {
|
updates.set(id, {
|
||||||
@@ -28,17 +25,13 @@ export function useResizeObserver() {
|
|||||||
|
|
||||||
updateNodeInternals(updates);
|
updateNodeInternals(updates);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
resizeObserverRef.current = observer;
|
|
||||||
|
|
||||||
return observer;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
resizeObserverRef?.current?.disconnect();
|
resizeObserver?.disconnect();
|
||||||
};
|
};
|
||||||
}, []);
|
}, [resizeObserver]);
|
||||||
|
|
||||||
return resizeObserver;
|
return resizeObserver;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user