skills/reactflow-expert/SKILL.md
Builds DAG visualizations using ReactFlow v12 with custom nodes, ELKjs auto-layout, Zustand state management, and live state updates via WebSocket. Use when implementing workflow visualization dashboards, creating custom agent node components, integrating ELK layout algorithms, or wiring execution state into React components. Activate on "ReactFlow", "workflow visualization", "DAG visualization", "ELKjs", "custom nodes", "node-based editor", "graph visualization". NOT for writing Mermaid diagrams (use mermaid-graph-writer), general React development, or static diagram rendering.
npx skillsauth add curiositech/windags-skills reactflow-expertInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Builds DAG visualizations using ReactFlow v12 with custom agent nodes, ELKjs auto-layout, Zustand state management, and live execution state updates.
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)
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
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
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
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
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 } }
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
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
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
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" onClick={() => pauseAgent(data.id)}>
Pause
</button>
<Handle type="source" position={Position.Bottom} />
</div>
);
}
Performance Tradeoffs Demonstrated:
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.
tools
Building resilient distributed systems with circuit breakers, retries with full-jitter exponential backoff, retry budgets (per-request 3-attempt + per-client 10% ratio per Google SRE), deadline propagation, and the cascading-failure math (4 layers × 3 retries = 64x amplification). Grounded in Resilience4j, Microsoft Cloud Patterns, AWS Architecture Blog (Marc Brooker), and Google SRE Book.
testing
Designing HTTP cache headers that work correctly across browsers, CDNs, and shared proxies — `Cache-Control` directives per RFC 9111, `stale-while-revalidate` and `stale-if-error` per RFC 5861, the Vary header for varying responses, and surrogate keys for tag-based purging. Grounded in IETF RFCs and Cloudflare/Fastly docs.
development
Use when designing or fixing a Content Security Policy on a real site, choosing between nonce-based and hash-based CSP, adding strict-dynamic, debugging "Refused to execute inline script" errors, deploying CSP in report-only mode first, configuring report-to / report-uri, or auditing an existing policy for unsafe-inline / unsafe-eval / wildcards. Triggers: "CSP blocks legitimate inline script", strict-dynamic, nonce-{RANDOM}, sha256-{HASH}, object-src none, base-uri none, frame-ancestors, Trusted Types, X-Content-Security-Policy obsolete, report-only vs enforced. NOT for general HTTP security headers (HSTS, COOP/COEP), Trusted Types deep dive, CORS configuration, or building a WAF.
tools
Choosing and operating an HTTP API versioning strategy that doesn't break clients — Stripe's date-based pinned versions, the Deprecation/Sunset header pair (RFC 9745 + RFC 8594), URI vs header vs media-type approaches, and the version-transformer pattern. Grounded in Stripe's published architecture and IETF RFCs.