ReactFlow Expert
Builds DAG visualizations using ReactFlow v12 with custom agent nodes, ELKjs auto-layout, Zustand state management, and live execution state updates.
Decision Points
State Management Strategy Selection
Graph Size <= 50 nodes?
├─ YES: Use useNodesState/useEdgesState hooks (simpler)
└─ NO: Use Zustand store
├─ Real-time updates required? → Include WebSocket integration
├─ Multi-component access? → Global Zustand store
└─ Complex interactions? → Add action methods (updateNodeData, bulkUpdate)
Layout Algorithm by DAG Shape
Node Count:
├─ < 20 nodes: Use 'layered' algorithm with direction='DOWN'
├─ 20-100 nodes: Use 'layered' with direction='RIGHT'
├─ > 100 nodes: Use 'stress' algorithm (better for large graphs)
└─ Highly connected (edges > 2x nodes): Use 'force' algorithm
Aspect Ratio:
├─ Wide dashboard: direction='RIGHT'
├─ Tall sidebar: direction='DOWN'
└─ Square viewport: Let ELK choose optimal direction
Custom Node Complexity Decision
Node Data Fields:
├─ Only status + name: Use built-in node types with custom styling
├─ 3-5 fields: Custom node with simple layout
├─ 6+ fields or nested data: Custom node with collapsible sections
└─ Interactive elements: Custom node + "nodrag" className on controls
Sync Strategy for Live Updates
Update Frequency:
├─ Real-time (< 1s): WebSocket with optimistic updates
├─ Frequent (1-10s): WebSocket with batching
├─ Periodic (> 10s): HTTP polling
└─ User-triggered: Manual refresh button
Data Size:
├─ Full DAG < 1MB: Send complete state
├─ Large DAG: Send delta updates (node ID + changed fields)
└─ Huge DAG: Implement viewport-based loading
Failure Modes
Infinite Re-render Loop
Symptom: Browser tab freezes, React DevTools shows constant re-renders
Detection: If nodeTypes object is defined inside component body
Fix: Move nodeTypes outside component or wrap in useMemo
Stale State Updates
Symptom: Node status changes don't appear visually, but store updates correctly
Detection: If mutating existing node objects instead of creating new ones
Fix: Always spread objects: { ...node, data: { ...node.data, newField } }
Layout Thrashing
Symptom: Nodes jump around constantly, poor performance with live updates Detection: If ELK layout runs on every state change instead of topology changes Fix: Only trigger layout when nodes/edges are added/removed, not data updates
Handle Positioning Mismatch
Symptom: Edges connect to wrong positions or don't connect at all
Detection: If using v11 position properties (xPos, yPos) in v12
Fix: Update to v12 properties: positionAbsoluteX, positionAbsoluteY
Drag Interference
Symptom: Buttons/inputs inside custom nodes trigger node dragging
Detection: If interactive elements don't have proper event handling
Fix: Add className="nodrag" to all buttons, inputs, selects inside nodes
Worked Examples
Complete WebSocket DAG Sync → ELK Layout → Custom Node Render
Scenario: Build a live workflow dashboard showing agent execution status
Step 1: Setup Zustand Store with WebSocket Integration
const useDAGStore = create<DAGStore>((set, get) => ({
nodes: [],
edges: [],
onNodesChange: (changes) => set({ nodes: applyNodeChanges(changes, get().nodes) }),
onEdgesChange: (changes) => set({ edges: applyEdgeChanges(changes, get().edges) }),
updateNodeData: (nodeId, data) => {
// EXPERT MOVE: Create new object to trigger React re-render
set({
nodes: get().nodes.map((n) =>
n.id === nodeId ? { ...n, data: { ...n.data, ...data } } : n
),
});
},
}));
Step 2: WebSocket Handler (Novice Miss: Updating in place)
function useDAGStream(dagId: string) {
const updateNodeData = useDAGStore((s) => s.updateNodeData);
useEffect(() => {
const ws = new WebSocket(`ws://api/dag/${dagId}/stream`);
ws.onmessage = (event) => {
const update = JSON.parse(event.data);
if (update.type === 'node_status') {
// EXPERT: Only update data, don't re-layout
updateNodeData(update.nodeId, {
status: update.status,
metrics: update.metrics
});
} else if (update.type === 'topology_change') {
// EXPERT: Only NOW do we re-layout
triggerLayout();
}
};
}, [dagId, updateNodeData]);
}
Step 3: ELK Layout with Performance Optimization
const useAutoLayout = () => {
const { fitView } = useReactFlow();
const { nodes, edges, setNodes } = useDAGStore();
return useCallback(async (direction = 'DOWN') => {
// EXPERT: Check node count and choose algorithm
const algorithm = nodes.length > 100 ? 'stress' : 'layered';
const layouted = await elk.layout({
id: 'root',
layoutOptions: {
'elk.algorithm': algorithm,
'elk.direction': direction,
'elk.spacing.nodeNode': nodes.length > 50 ? '60' : '80',
},
children: nodes.map((n) => ({
...n,
// EXPERT: Pass measured dimensions to prevent layout jumps
width: n.measured?.width ?? 220,
height: n.measured?.height ?? 120,
})),
edges,
});
const positioned = layouted.children!.map((elkN) => ({
...nodes.find((n) => n.id === elkN.id)!,
position: { x: elkN.x!, y: elkN.y! },
}));
setNodes(positioned);
// EXPERT: Defer fitView until after DOM update
requestAnimationFrame(() => fitView());
}, [nodes, edges, setNodes, fitView]);
};
Step 4: Custom Agent Node with Status Colors
// EXPERT: Define outside component to prevent re-registration
const nodeTypes = { agentNode: AgentNode };
function AgentNode({ data }: NodeProps) {
return (
<div className={`agent-node status-${data.status}`}>
<Handle type="target" position={Position.Top} />
<div className="node-header">
<span className={`status-dot ${data.status}`} />
<span>{data.role}</span>
</div>
{data.status === 'running' && data.progress && (
<div className="progress-bar">
<div style={{ width: `${data.progress}%` }} />
</div>
)}
{/* EXPERT: Interactive elements need nodrag */}
<button className="nodrag" => pauseAgent(data.id)}>
Pause
</button>
<Handle type="source" position={Position.Bottom} />
</div>
);
}
Performance Tradeoffs Demonstrated:
- Zustand vs useState: Zustand handles 100+ node updates without re-rendering parent
- ELK algorithm choice: 'layered' fast for < 100 nodes, 'stress' for larger graphs
- WebSocket batching: Group status updates to avoid layout thrashing
Quality Gates
Functional Validation
- ReactFlow component renders without console errors
- All custom node types appear in nodeTypes registry
- ELK layout completes within 2 seconds for target node count
- WebSocket connections establish and receive test messages
- Node data updates trigger visual changes within 200ms
Performance Thresholds
- Initial render completes within 1 second for 50 nodes
- Layout computation finishes within 3 seconds for 100 nodes
- Memory usage stays under 100MB for 200-node graphs
- Frame rate maintains 30fps during zoom/pan operations
- WebSocket updates don't cause visible stuttering
Visual Quality Checks
- Edges connect to correct handle positions (not node centers)
- Node labels remain readable at 50% zoom level
- Status colors match design system specifications
- Interactive elements (buttons, inputs) don't trigger node dragging
- fitView() centers graph with appropriate padding margins
Accessibility Requirements
- All interactive nodes have proper ARIA labels
- Keyboard navigation works for node selection
- Color-based status indicators have text/icon alternatives
- Focus indicators visible on custom node components
- Screen reader announces node status changes
NOT-FOR Boundaries
Static Diagrams: For Mermaid flowcharts or architectural diagrams, use mermaid-graph-writer instead. ReactFlow is for interactive, live-updating visualizations.
Simple Charts: For bar charts, line graphs, or pie charts, use dedicated charting libraries (recharts, d3) instead. ReactFlow is specifically for node-edge graphs.
General React Development: For standard React components, forms, or layouts, use general React skills instead. This skill is ReactFlow-specific.
3D Visualizations: For 3D network graphs or spatial layouts, use three.js or WebGL libraries instead. ReactFlow is 2D-only.
Mobile-First Apps: For touch-first mobile interfaces, consider native gestures instead. ReactFlow is optimized for mouse/trackpad interaction.
Real-time Collaboration: For multi-user editing like Figma, use specialized real-time sync libraries instead. ReactFlow handles read-only live updates well but not collaborative editing.