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.
data-ai
license: Apache-2.0 NOT for unrelated tasks outside this domain.
development
Use when designing caching strategies (cache-aside, write-through, write-behind), implementing distributed locks, building rate limiters, leaderboards, real-time streams (XADD/consumer groups), pub/sub, or tuning eviction policies. Triggers: thundering-herd on cache miss, dogpile on key expiry, Redlock vs SET-NX-PX choice, sliding-window rate limiter, hot-key on a single cluster slot, big-key blowup, MULTI/EXEC across slots, KEYS in production. NOT for Redis Cluster operations/admin (different domain), embedded KV (SQLite, leveldb), in-process LRU caches, or Memcached.
tools
Drawing the `'use client'` boundary correctly in React Server Components apps (Next.js App Router, RSC frameworks) — leaf-pushing, slot composition, serialization rules, and environment poisoning prevention. Grounded in react.dev and Next.js 16 docs.
development
Use when designing rate limiting for an API, choosing between token bucket / sliding window / leaky bucket / fixed window, implementing it in Redis, deciding edge (Cloudflare/Upstash) vs origin enforcement, sizing per-user vs per-IP vs per-endpoint quotas, returning the right 429 response with Retry-After, or fixing the boundary-burst bug in fixed-window limiters. Triggers: 429 too many requests, INCR + EXPIRE, ZADD + ZREMRANGEBYSCORE + ZCARD, X-RateLimit-Remaining header, Cloudflare WAF rate limiting rules, Upstash @upstash/ratelimit, leaky bucket shaping vs policing, distributed rate limiter consistency. NOT for DDoS mitigation specifically (different scale), CAPTCHA / bot management, full WAF design, or per-user quota billing.