diff --git a/examples/react/src/examples/DevTools/ReactFlowDevTools/ChangeLogger.tsx b/examples/react/src/examples/DevTools/ReactFlowDevTools/ChangeLogger.tsx
new file mode 100644
index 00000000..7a137e33
--- /dev/null
+++ b/examples/react/src/examples/DevTools/ReactFlowDevTools/ChangeLogger.tsx
@@ -0,0 +1,82 @@
+import { useEffect, useRef, useState } from 'react';
+import { NodeChange, OnNodesChange, useStore, useStoreApi } from '@xyflow/react';
+
+type ChangeLoggerProps = {
+ color?: string;
+ limit?: number;
+};
+
+type ChangeInfoProps = {
+ change: NodeChange;
+};
+
+function ChangeInfo({ change, index }: ChangeInfoProps) {
+ const id = 'id' in change ? change.id : '-';
+ const { type } = change;
+
+ return (
+
+
node id: {id}
+
+ {type === 'add' ? JSON.stringify(change.item, null, 2) : null}
+ {type === 'dimensions' ? `${change.dimensions?.width} × ${change.dimensions?.height}` : null}
+ {type === 'position' ? `position: ${change.position?.x.toFixed(1)}, ${change.position?.y.toFixed(1)}` : null}
+ {type === 'remove' ? 'remove' : null}
+ {type === 'replace' ? JSON.stringify(change.item, null, 2) : null}
+ {type === 'select' ? (change.selected ? 'select' : 'unselect') : null}
+
+
+ );
+}
+
+export default function ChangeLogger({ color = '#555', limit = 20 }: ChangeLoggerProps) {
+ const [changes, setChanges] = useState([]);
+ const onNodesChangeIntercepted = useRef(false);
+ const onNodesChange = useStore((s) => s.onNodesChange);
+ const store = useStoreApi();
+
+ useEffect(() => {
+ if (!onNodesChange || onNodesChangeIntercepted.current) {
+ return;
+ }
+
+ onNodesChangeIntercepted.current = true;
+ const userOnNodesChange = onNodesChange;
+
+ const onNodesChangeLogger: OnNodesChange = (changes) => {
+ userOnNodesChange(changes);
+
+ requestAnimationFrame(() => {
+ setChanges((c) => {
+ changes.forEach((change) => {
+ if (c.length >= limit) {
+ c.pop();
+ }
+
+ c = [change, ...c];
+ });
+ return c;
+ });
+ });
+ };
+
+ store.setState({ onNodesChange: onNodesChangeLogger });
+ }, [onNodesChange]);
+
+ return (
+
+
+ Change Logger
+
+
+ {changes.length === 0 ? (
+ <>no changes triggered>
+ ) : (
+ changes.map((change, index) => )
+ )}
+
+
+ );
+}
diff --git a/examples/react/src/examples/DevTools/ReactFlowDevTools/NodeInspector.tsx b/examples/react/src/examples/DevTools/ReactFlowDevTools/NodeInspector.tsx
index 34ebc361..ae07029a 100644
--- a/examples/react/src/examples/DevTools/ReactFlowDevTools/NodeInspector.tsx
+++ b/examples/react/src/examples/DevTools/ReactFlowDevTools/NodeInspector.tsx
@@ -1,40 +1,74 @@
import { useNodes, ViewportPortal } from '@xyflow/react';
+type NodeInfoProps = {
+ id: string;
+ type: string;
+ x: number;
+ y: number;
+ width?: number;
+ height?: number;
+ data: any;
+};
+
+function NodeInfo({ id, type, x, y, width, height, data }: NodeInfoProps) {
+ if (!width || !height) {
+ return ;
+ }
+
+ return (
+
+
id: {id}
+
type: {type}
+
+ position: {x.toFixed(1)}, {y.toFixed(1)}
+
+
+ dimensions: {width} × {height}
+
+
data: {JSON.stringify(data, null, 2)}
+
+ );
+}
+
type NodeInspectorProps = {
color: string;
};
-export default function NodeInspector({ color = '#aaa' }: NodeInspectorProps) {
+export default function NodeInspector({ color = '#555' }: NodeInspectorProps) {
const nodes = useNodes();
return (
- {nodes.map((node) => {
- const x = node.computed?.positionAbsolute?.x || 0;
- const y = node.computed?.positionAbsolute?.y || 0;
- const height = node.computed?.height || 0;
+
+ {nodes.map((node) => {
+ const x = node.computed?.positionAbsolute?.x || 0;
+ const y = node.computed?.positionAbsolute?.y || 0;
+ const width = node.computed?.width || 0;
+ const height = node.computed?.height || 0;
- return (
-
-
id: {node.id}
-
type: {node.type}
-
- position: {x.toFixed(1)}, {y.toFixed(1)}
-
-
data: {JSON.stringify(node.data, null, 2)}
-
- );
- })}
+ return (
+
+ );
+ })}
+
);
}
diff --git a/examples/react/src/examples/DevTools/ReactFlowDevTools/index.tsx b/examples/react/src/examples/DevTools/ReactFlowDevTools/index.tsx
index 8c521aa0..2b488b7c 100644
--- a/examples/react/src/examples/DevTools/ReactFlowDevTools/index.tsx
+++ b/examples/react/src/examples/DevTools/ReactFlowDevTools/index.tsx
@@ -2,6 +2,7 @@ import { useState } from 'react';
import { Panel, PanelPosition } from '@xyflow/react';
import NodeInspector from './NodeInspector';
+import ChangeLogger from './ChangeLogger';
type ReactFlowDevToolsProps = {
position?: PanelPosition;
@@ -10,13 +11,16 @@ type ReactFlowDevToolsProps = {
export default function ReactFlowDevTools({ color = '#aaa', position = 'top-left' }: ReactFlowDevToolsProps) {
const [isNodeInspectorActive, setIsNodeInspectorActive] = useState(false);
+ const [isChangeLoggerActive, setIsChangeLoggerActive] = useState(false);
return (
<>
+
{isNodeInspectorActive && }
+ {isChangeLoggerActive && }
>
);
}