| name | react-flow-advanced |
| description | Advanced React Flow patterns for complex use cases. Use when implementing sub-flows, custom connection lines, programmatic layouts, drag-and-drop, undo/redo, or complex state synchronization. |
Advanced React Flow Patterns
Gates (check before shipping)
Use these as sequenced checks—not “I think it works.”
- Sub-flows / groups: Pass: Every
parentId matches an existing node id; the parent type is registered in nodeTypes; child positions are relative to the parent as intended (spot-check one drag inside/outside the group).
- Custom connection line: Pass: With a valid/invalid drag, stroke or
connectionStatus visibly differs; path renders without console errors from getSmoothStepPath (invalid coords).
- External drag-and-drop: Pass:
onDragOver always preventDefault(); drop position uses screenToFlowPosition (not raw clientX/clientY as flow coords); new node appears under the cursor on the pane.
- Undo/redo: Pass: One undo returns to the prior
{ nodes, edges }; redo restores; rapid changes do not leave canUndo/canRedo inconsistent with visible graph (exercise add → undo → redo once).
- Programmatic layout (dagre): Pass: After
setNodes, node positions match intended rankdir; fitView runs after layout (e.g. requestAnimationFrame) so the viewport is not stale.
- Connect on drop (new node): Pass: Dropping on empty pane creates a node and an edge from the source handle; dropping on a valid target does not duplicate nodes (only the invalid-drop path adds a node).
- Selectors / store: Pass: Components that
useStore with objects use shallow (or equivalent) so unrelated store updates do not re-render every frame.
Sub-Flows (Nested Nodes)
const nodes = [
{
id: 'group-1',
type: 'group',
position: { x: 0, y: 0 },
style: { width: 400, height: 300, padding: 10 },
data: { label: 'Group' },
},
{
id: 'child-1',
parentId: 'group-1',
extent: 'parent',
expandParent: true,
position: { x: 20, y: 50 },
data: { label: 'Child 1' },
},
{
id: 'child-2',
parentId: 'group-1',
extent: 'parent',
position: { x: 200, y: 50 },
data: { label: },
},
];
Group Node Component
function GroupNode({ data, id }: NodeProps) {
return (
<div className="group-node">
<div className="group-header">{data.label}</div>
{/* Children are rendered automatically by React Flow */}
</div>
);
}
Custom Connection Line
import { ConnectionLineComponentProps, getSmoothStepPath } from '@xyflow/react';
function CustomConnectionLine({
fromX, fromY, fromPosition,
toX, toY, toPosition,
connectionStatus,
}: ConnectionLineComponentProps) {
const [path] = getSmoothStepPath({
sourceX: fromX,
sourceY: fromY,
sourcePosition: fromPosition,
targetX: toX,
targetY: toY,
targetPosition: toPosition,
});
return (
<g>
<path
d={path}
fill="none"
stroke={connectionStatus === 'valid' ? '#22c55e' : '#ef4444'}
strokeWidth={2}
strokeDasharray="5 5"
/>
</g>
);
}
<ReactFlow connectionLineComponent={CustomConnectionLine} />
Drag and Drop from External Source
import { useCallback, useRef, useState } from 'react';
import { useReactFlow } from '@xyflow/react';
function DnDFlow() {
const reactFlowWrapper = useRef(null);
const { screenToFlowPosition, addNodes } = useReactFlow();
const [reactFlowInstance, setReactFlowInstance] = useState(null);
const onDragOver = useCallback((event: DragEvent) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
}, []);
const onDrop = useCallback((event: DragEvent) => {
event.preventDefault();
const type = event.dataTransfer.getData('application/reactflow');
if (!type) return;
const position = screenToFlowPosition({
x: event.clientX,
y: event.clientY,
});
newNode = {
: ,
,
position,
: { : },
};
(newNode);
}, [screenToFlowPosition, addNodes]);
(
);
}
() {
= () => {
event..(, nodeType);
event.. = ;
};
(
);
}
Undo/Redo
import { useCallback, useState } from 'react';
function useUndoRedo<T>(initialState: T) {
const [history, setHistory] = useState<T[]>([initialState]);
const [index, setIndex] = useState(0);
const state = history[index];
const setState = useCallback((newState: T | ((prev: T) => T)) => {
setHistory((prev) => {
const resolved = typeof newState === 'function'
? (newState as (prev: T) => T)(prev[index])
: newState;
const newHistory = prev.slice(0, index + 1);
return [...newHistory, resolved];
});
setIndex((i) => i + 1);
}, [index]);
const undo = useCallback(() => {
setIndex((i) => Math.max(0, i - 1));
}, []);
const redo = useCallback(() => {
setIndex((i) => .(history. - , i + ));
}, [history.]);
canUndo = index > ;
canRedo = index < history. - ;
{ state, setState, undo, redo, canUndo, canRedo };
}
() {
{
: { nodes, edges },
setState,
undo, redo, canUndo, canRedo
} = ({ : initialNodes, : initialEdges });
onNodesChange = ( {
hasPositionChange = changes.( c. === && !c.);
(hasPositionChange) {
( ({
: (changes, prev.),
: prev.,
}));
}
}, [setState]);
}
Programmatic Layout with dagre
import dagre from 'dagre';
interface LayoutOptions {
direction: 'TB' | 'BT' | 'LR' | 'RL';
nodeWidth: number;
nodeHeight: number;
}
function getLayoutedElements(
nodes: Node[],
edges: Edge[],
options: LayoutOptions = { direction: 'TB', nodeWidth: 172, nodeHeight: 36 }
) {
const g = new dagre.graphlib.Graph();
g.setGraph({ rankdir: options.direction });
g.setDefaultEdgeLabel(() => ({}));
nodes.forEach((node) => {
g.setNode(node.id, {
width: node.measured?.width ?? options.nodeWidth,
height: node.measured?.height ?? options.nodeHeight,
});
});
edges.forEach((edge) => {
g.setEdge(edge., edge.);
});
dagre.(g);
layoutedNodes = nodes.( {
nodeWithPosition = g.(node.);
{
...node,
: {
: nodeWithPosition. - (node.?. ?? options.) / ,
: nodeWithPosition. - (node.?. ?? options.) / ,
},
};
});
{ : layoutedNodes, edges };
}
() {
{ fitView } = ();
onLayout = ( {
{ : layoutedNodes, : layoutedEdges } = (
nodes,
edges,
{ direction, : , : }
);
([...layoutedNodes]);
([...layoutedEdges]);
.( {
({ : });
});
}, [nodes, edges, setNodes, setEdges, fitView]);
}
Connection with Edge on Drop
function Flow() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const { screenToFlowPosition } = useReactFlow();
const onConnectEnd = useCallback(
(event: MouseEvent | TouchEvent, connectionState: FinalConnectionState) => {
if (!connectionState.isValid && connectionState.fromHandle) {
const id = `${Date.now()}`;
const { clientX, clientY } = 'changedTouches' in event
? event.changedTouches[0]
: event;
const newNode = {
id,
position: screenToFlowPosition({ x: clientX, y: clientY }),
data: { label: 'New Node' },
};
setNodes((nds) => [...nds, newNode]);
setEdges((eds) => [
...eds,
{
id: `e--`,
: connectionState.?. ?? ,
: id,
},
]);
}
},
[screenToFlowPosition, setNodes, setEdges]
);
(
);
}
Accessing Node Data from Edges
import { useNodesData, type EdgeProps } from '@xyflow/react';
function DataEdge({ source, target, ...props }: EdgeProps) {
const nodesData = useNodesData([source, target]);
const sourceData = nodesData[0];
const targetData = nodesData[1];
const [path, labelX, labelY] = getSmoothStepPath(props);
return (
<>
<BaseEdge path={path} />
<EdgeLabelRenderer>
<div style={{ transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)` }}>
{sourceData?.data?.label} → {targetData?.data?.label}
</div>
</EdgeLabelRenderer>
</>
);
}
Middleware for Node Changes
const onNodesChangeMiddleware = useCallback((changes: NodeChange[]) => {
const filteredChanges = changes.filter((change) => {
if (change.type === 'remove') {
const node = nodes.find((n) => n.id === change.id);
return node?.data?.deletable !== false;
}
return true;
});
setNodes((nds) => applyNodeChanges(filteredChanges, nds));
}, [nodes, setNodes]);
Keyboard Shortcuts
import { useKeyPress } from '@xyflow/react';
function Flow() {
const { deleteElements, getNodes, getEdges, fitView } = useReactFlow();
const selectAllPressed = useKeyPress(['Meta+a', 'Control+a']);
useEffect(() => {
if (selectAllPressed) {
setNodes((nds) => nds.map((n) => ({ ...n, selected: true })));
setEdges((eds) => eds.map((e) => ({ ...e, selected: true })));
}
}, [selectAllPressed]);
const deletePressed = useKeyPress(['Backspace', 'Delete']);
useEffect(() => {
if (deletePressed) {
const selectedNodes = getNodes().filter((n) => n.selected);
const selectedEdges = getEdges().filter(() => e.);
({ : selectedNodes, : selectedEdges });
}
}, [deletePressed]);
}
Performance: Memoizing Selectors
import { useCallback } from 'react';
import { useStore, type ReactFlowState } from '@xyflow/react';
import { shallow } from 'zustand/shallow';
const nodesSelector = (state: ReactFlowState) => state.nodes;
const flowStateSelector = (state: ReactFlowState) => ({
nodes: state.nodes,
edges: state.edges,
viewport: state.transform,
});
function FlowInfo() {
const { nodes, edges, viewport } = useStore(flowStateSelector, shallow);
return <div>Nodes: {nodes.length}, Edges: {edges.length}</div>;
}