react-flow-advanced — Claude Code skill: what it does, how to install
Guides your coding agent through complex React Flow implementations, including nested sub-flows, dagre layouts, drag-and-drop, and undo/redo history.
npx skills add existential-birds/beagle --skill react-flow-advanced -a claude-codeWhat react-flow-advanced does
This skill equips agents with concrete patterns and verification gates for @xyflow/react. It tackles non-trivial graph features: nested group nodes that expand dynamically, custom connection lines that reflect validation state, and external palette drops. Complex graphs break easily.
The instructions emphasize strict verification checks before shipping. Dropped elements must convert cursor coordinates via screenToFlowPosition rather than raw client coordinates. Automatic tree layouts rely on dagre, with fitView queued via requestAnimationFrame to avoid rendering stale viewport boundaries.
Graph history tracking is handled by a dedicated useUndoRedo hook capturing node positions only after dragging finishes. Without shallow comparisons in useStore selectors, arbitrary graph updates trigger frame-by-frame re-renders across the entire canvas.
What it helps with
- Configure nested group nodes with strict parent-child coordinate bounds
- Implement drag-and-drop from an external palette using
screenToFlowPosition - Add step-by-step undo and redo state management for graph elements
- Calculate programmatic node layouts using
dagre
Example prompts for the agent
- Implement external drag-and-drop for our React Flow canvas.
- Add an undo and redo history hook for node changes.
- Set up a hierarchical layout using dagre and fit the view properly.
How to install react-flow-advanced
1Skills CLI (Vercel)
npx skills add existential-birds/beagle --skill react-flow-advanced -a claude-code2Manually: put the files in place
~/.claude/skills/react-flow-advanced/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/react-flow-advanced/
1Skills CLI (Vercel)
npx skills add existential-birds/beagle --skill react-flow-advanced -a codex2Manually: put the files in place
.agents/skills/react-flow-advanced/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add existential-birds/beagle --skill react-flow-advanced -a cursor2Manually: put the files in place
.cursor/skills/react-flow-advanced/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install existential-birds/beagle react-flow-advanced2Manually: put the files in place
.github/skills/react-flow-advanced/SKILL.md1Skills CLI (Vercel)
npx skills add existential-birds/beagle --skill react-flow-advanced -a gemini-cli2Platform CLI
gemini skills install https://github.com/existential-birds/beagle --path plugins/beagle-react/skills/react-flow-advancedManually: put the files in place
.devin/skills/react-flow-advanced/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add existential-birds/beagle --skill react-flow-advanced -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about react-flow-advanced
How do I install react-flow-advanced in Claude Code?
Run npx skills add existential-birds/beagle --skill react-flow-advanced -a claude-code in your terminal.
What does react-flow-advanced do?
It gives coding assistants tested implementations for advanced graph features in @xyflow/react, such as sub-flows, auto-layout, and state history.
How does it handle coordinate conversion during drag-and-drop?
The skill specifies using screenToFlowPosition from useReactFlow, preventing node displacement caused by unmapped raw client coordinates.
Source text
SKILL.md · Apache-2.0
---
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.”
1. **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).
2. **Custom connection line:** **Pass:** With a valid/invalid drag, stroke or `connectionStatus` visibly differs; path renders without console errors from `getSmoothStepPath` (invalid coords).
3. **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.
4. **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).
5. **Programmatic layout (dagre):** **Pass:** After `setNodes`, node positions match intended `rankdir`; `fitView` runs after layout (e.g. `requestAnimationFrame`) so the viewport is not stale.
6. **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).
7. **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)
```tsx
const nodes = [
// Parent (group) node
{
id: 'group-1',
type: 'group',
position: { x: 0, y: 0 },
style: { width: 400, height: 300, padding: 10 },
data: { label: 'Group' },
},
// Child nodes
{
id: 'child-1',
parentId: 'group-1', // Reference parent
extent: 'parent', // Constrain to parent bounds
expandParent: true, // Auto-expand parent if dragged to edge
position: { x: 20, y: 50 }, // Relative to parent
data: { label: 'Child 1' },
},
{
id: 'child-2',
parentId: 'group-1',
extent: 'parent',
position: { x: 200, y: 50 },
data: { label: 'Child 2' },
},
];
```
### Group Node Component
```tsx
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
```tsx
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
```tsx
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;
// Convert screen position to flow position
const position = screenToFlowPosition({
x: event.clientX,
y: event.clientY,
});
const newNode = {
id: `${Date.now()}`,
type,
position,
data: { label: `${type} node` },
};
addNodes(newNode);
}, [screenToFlowPosition, addNodes]);
return (
<div ref={reactFlowWrapper} style={{ height: '100%' }}>
<ReactFlow
onDragOver={onDragOver}
onDrop={onDrop}
onInit={setReactFlowInstance}
/>
</div>
);
}
// Sidebar component
function Sidebar() {
const onDragStart = (event: DragEvent, nodeType: string) => {
event.dataTransfer.setData('application/reactflow', nodeType);
event.dataTransfer.effectAllowed = 'move';
};
return (
<aside>
<div draggable onDragStart={(e) => onDragStart(e, 'input')}>
Input Node
</div>
<div draggable onDragStart={(e) => onDragStart(e, 'default')}>
Default Node
</div>
</aside>
);
}
```
## Undo/Redo
```tsx
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;
// Remove future states and add new state
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) => Math.min(history.length - 1, i + 1));
}, [history.length]);
const canUndo = index > 0;
const canRedo = index < history.length - 1;
return { state, setState, undo, redo, canUndo, canRedo };
}
// Usage
function Flow() {
const {
state: { nodes, edges },
setState,
undo, redo, canUndo, canRedo
} = useUndoRedo({ nodes: initialNodes, edges: initialEdges });
// Capture state on significant changes
const onNodesChange = useCallback((changes) => {
const hasPositionChange = changes.some(c => c.type === 'position' && !c.dragging);
if (hasPositionChange) {
setState(prev => ({
nodes: applyNodeChanges(changes, prev.nodes),
edges: prev.edges,
}));
}
}, [setState]);
}
```
## Programmatic Layout with dagre
```tsx
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.source, edge.target);
});
dagre.layout(g);
const layoutedNodes = nodes.map((node) => {
const nodeWithPosition = g.node(node.id);
return {
...node,
position: {
x: nodeWithPosition.x - (node.measured?.width ?? options.nodeWidth) / 2,
y: nodeWithPosition.y - (node.measured?.height ?? options.nodeHeight) / 2,
},
};
});
return { nodes: layoutedNodes, edges };
}
// Usage after nodes are measured
function Flow() {
const { fitView } = useReactFlow();
const onLayout = useCallback((direction: 'TB' | 'LR') => {
const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(
nodes,
edges,
{ direction, nodeWidth: 150, nodeHeight: 50 }
);
setNodes([...layoutedNodes]);
setEdges([...layoutedEdges]);
window.requestAnimationFrame(() => {
fitView({ duration: 500 });
});
}, [nodes, edges, setNodes, setEdges, fitView]);
}
```
## Connection with Edge on Drop
```tsx
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) => {
// Only proceed if dropped on pane (not on a node)
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.fromNode?.id}-${id}`,
source: connectionState.fromNode?.id ?? '',
target: id,
},
]);
}
},
[screenToFlowPosition, setNodes, setEdges]
);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnectEnd={onConnectEnd}
/>
);
}
```
## Accessing Node Data from Edges
```tsx
import { useNodesData, type EdgeProps } from '@xyflow/react';
function DataEdge({ source, target, ...props }: EdgeProps) {
// Get data for source and target nodes
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
```tsx
// Filter or modify changes before they're applied
const onNodesChangeMiddleware = useCallback((changes: NodeChange[]) => {
// Example: Prevent deletion of certain nodes
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
```tsx
import { useKeyPress } from '@xyflow/react';
function Flow() {
const { deleteElements, getNodes, getEdges, fitView } = useReactFlow();
// Ctrl/Cmd + A: Select all
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]);
// Custom delete handler
const deletePressed = useKeyPress(['Backspace', 'Delete']);
useEffect(() => {
if (deletePressed) {
const selectedNodes = getNodes().filter((n) => n.selected);
const selectedEdges = getEdges().filter((e) => e.selected);
deleteElements({ nodes: selectedNodes, edges: selectedEdges });
}
}, [deletePressed]);
}
```
## Performance: Memoizing Selectors
```tsx
import { useCallback } from 'react';
import { useStore, type ReactFlowState } from '@xyflow/react';
import { shallow } from 'zustand/shallow';
// Create stable selector outside component
const nodesSelector = (state: ReactFlowState) => state.nodes;
// Or use multiple values with shallow compare
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>;
}
```
© existential-birds · Apache-2.0 · existential-birds/beagle