From 1e797799a93090670f5c03b31bb10522c2066cc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Ba=C5=9Fkurt?= <73551559+mertbaskurt@users.noreply.github.com> Date: Mon, 16 Feb 2026 15:26:18 +0300 Subject: [PATCH] feat: add react-flow-architect skill (#88) - Expert ReactFlow architect for interactive graph applications - Hierarchical navigation with expand/collapse patterns - Performance optimization with incremental rendering - State management with reducer and history - Auto-layout integration with Dagre - Focus mode and search functionality - Complete production-ready examples --- skills/react-flow-architect/SKILL.md | 602 +++++++++++++++++++++++++++ 1 file changed, 602 insertions(+) create mode 100644 skills/react-flow-architect/SKILL.md diff --git a/skills/react-flow-architect/SKILL.md b/skills/react-flow-architect/SKILL.md new file mode 100644 index 0000000..4db9537 --- /dev/null +++ b/skills/react-flow-architect/SKILL.md @@ -0,0 +1,602 @@ +--- +name: react-flow-architect +description: Expert ReactFlow architect for building interactive graph applications with hierarchical node-edge systems, performance optimization, and auto-layout integration. Use when Claude needs to create or optimize ReactFlow applications for: (1) Interactive process graphs with expand/collapse navigation, (2) Hierarchical tree structures with drag & drop, (3) Performance-optimized large datasets with incremental rendering, (4) Auto-layout integration with Dagre, (5) Complex state management for nodes and edges, or any advanced ReactFlow visualization requirements. +--- + +# ReactFlow Architect + +Build production-ready ReactFlow applications with hierarchical navigation, performance optimization, and advanced state management. + +## Quick Start + +Create basic interactive graph: + +```tsx +import ReactFlow, { Node, Edge } from 'reactflow'; + +const nodes: Node[] = [ + { id: '1', position: { x: 0, y: 0 }, data: { label: 'Node 1' } }, + { id: '2', position: { x: 100, y: 100 }, data: { label: 'Node 2' } } +]; + +const edges: Edge[] = [ + { id: 'e1-2', source: '1', target: '2' } +]; + +export default function Graph() { + return ( + + ); +} +``` + +## Core Patterns + +### Hierarchical Tree Navigation + +Build expandable/collapsible tree structures with parent-child relationships. + +#### Node Schema +```typescript +interface TreeNode extends Node { + data: { + label: string; + level: number; + hasChildren: boolean; + isExpanded: boolean; + childCount: number; + category: 'root' | 'category' | 'process' | 'detail'; + }; +} +``` + +#### Incremental Node Building +```typescript +const buildVisibleNodes = useCallback(( + allNodes: TreeNode[], + expandedIds: Set, + otherDeps: any[] +) => { + const visibleNodes = new Map(); + const visibleEdges = new Map(); + + // Start with root nodes + const rootNodes = allNodes.filter(n => n.data.level === 0); + + // Recursively add visible nodes + const addVisibleChildren = (node: TreeNode) => { + visibleNodes.set(node.id, node); + + if (expandedIds.has(node.id)) { + const children = allNodes.filter(n => n.parentNode === node.id); + children.forEach(child => addVisibleChildren(child)); + } + }; + + rootNodes.forEach(root => addVisibleChildren(root)); + + return { nodes: Array.from(visibleNodes.values()), edges: Array.from(visibleEdges.values()) }; +}, []); +``` + +### Performance Optimization + +Handle large datasets with incremental rendering and memoization. + +#### Incremental Rendering +```typescript +const useIncrementalGraph = (allNodes: Node[], allEdges: Edge[], expandedList: string[]) => { + const prevExpandedListRef = useRef>(new Set()); + const prevOtherDepsRef = useRef([]); + + const { visibleNodes, visibleEdges } = useMemo(() => { + const currentExpandedSet = new Set(expandedList); + const prevExpandedSet = prevExpandedListRef.current; + + // Check if expanded list changed + const expandedChanged = !areSetsEqual(currentExpandedSet, prevExpandedSet); + + // Check if other dependencies changed + const otherDepsChanged = !arraysEqual(otherDeps, prevOtherDepsRef.current); + + if (expandedChanged && !otherDepsChanged) { + // Only expanded list changed - incremental update + return buildIncrementalUpdate( + cachedVisibleNodesRef.current, + cachedVisibleEdgesRef.current, + allNodes, + allEdges, + currentExpandedSet, + prevExpandedSet + ); + } else { + // Full rebuild needed + return buildFullGraph(allNodes, allEdges, currentExpandedSet); + } + }, [allNodes, allEdges, expandedList, ...otherDeps]); + + return { visibleNodes, visibleEdges }; +}; +``` + +#### Memoization Patterns +```typescript +// Memoize node components to prevent unnecessary re-renders +const ProcessNode = memo(({ data, selected }: NodeProps) => { + return ( +
+ {data.label} +
+ ); +}, (prevProps, nextProps) => { + // Custom comparison function + return ( + prevProps.data.label === nextProps.data.label && + prevProps.selected === nextProps.selected && + prevProps.data.isExpanded === nextProps.data.isExpanded + ); +}); + +// Memoize edge calculations +const styledEdges = useMemo(() => { + return edges.map(edge => ({ + ...edge, + style: { + ...edge.style, + strokeWidth: selectedEdgeId === edge.id ? 3 : 2, + stroke: selectedEdgeId === edge.id ? '#3b82f6' : '#94a3b8', + }, + animated: selectedEdgeId === edge.id, + })); +}, [edges, selectedEdgeId]); +``` + +### State Management + +Complex node/edge state patterns with undo/redo and persistence. + +#### Reducer Pattern +```typescript +type GraphAction = + | { type: 'SELECT_NODE'; payload: string } + | { type: 'SELECT_EDGE'; payload: string } + | { type: 'TOGGLE_EXPAND'; payload: string } + | { type: 'UPDATE_NODES'; payload: Node[] } + | { type: 'UPDATE_EDGES'; payload: Edge[] } + | { type: 'UNDO' } + | { type: 'REDO' }; + +const graphReducer = (state: GraphState, action: GraphAction): GraphState => { + switch (action.type) { + case 'SELECT_NODE': + return { + ...state, + selectedNodeId: action.payload, + selectedEdgeId: null, + }; + + case 'TOGGLE_EXPAND': + const newExpanded = new Set(state.expandedNodeIds); + if (newExpanded.has(action.payload)) { + newExpanded.delete(action.payload); + } else { + newExpanded.add(action.payload); + } + return { + ...state, + expandedNodeIds: newExpanded, + isDirty: true, + }; + + default: + return state; + } +}; +``` + +#### History Management +```typescript +const useHistoryManager = (state: GraphState, dispatch: Dispatch) => { + const canUndo = state.historyIndex > 0; + const canRedo = state.historyIndex < state.history.length - 1; + + const undo = useCallback(() => { + if (canUndo) { + const newIndex = state.historyIndex - 1; + const historyEntry = state.history[newIndex]; + + dispatch({ + type: 'RESTORE_FROM_HISTORY', + payload: { + ...historyEntry, + historyIndex: newIndex, + }, + }); + } + }, [canUndo, state.historyIndex, state.history]); + + const saveToHistory = useCallback(() => { + dispatch({ type: 'SAVE_TO_HISTORY' }); + }, [dispatch]); + + return { canUndo, canRedo, undo, redo, saveToHistory }; +}; +``` + +## Advanced Features + +### Auto-Layout Integration + +Integrate Dagre for automatic graph layout: + +```typescript +import dagre from 'dagre'; + +const layoutOptions = { + rankdir: 'TB', // Top to Bottom + nodesep: 100, // Node separation + ranksep: 150, // Rank separation + marginx: 50, + marginy: 50, + edgesep: 10, +}; + +const applyLayout = (nodes: Node[], edges: Edge[]) => { + const g = new dagre.graphlib.Graph(); + g.setGraph(layoutOptions); + g.setDefaultEdgeLabel(() => ({})); + + // Add nodes to graph + nodes.forEach(node => { + g.setNode(node.id, { width: 200, height: 100 }); + }); + + // Add edges to graph + edges.forEach(edge => { + g.setEdge(edge.source, edge.target); + }); + + // Calculate layout + dagre.layout(g); + + // Apply positions + return nodes.map(node => ({ + ...node, + position: { + x: g.node(node.id).x - 100, + y: g.node(node.id).y - 50, + }, + })); +}; + +// Debounce layout calculations +const debouncedLayout = useMemo( + () => debounce(applyLayout, 150), + [] +); +``` + +### Focus Mode + +Isolate selected nodes and their direct connections: + +```typescript +const useFocusMode = (selectedNodeId: string, allNodes: Node[], allEdges: Edge[]) => { + return useMemo(() => { + if (!selectedNodeId) return { nodes: allNodes, edges: allEdges }; + + // Get direct connections + const connectedNodeIds = new Set([selectedNodeId]); + const focusedEdges: Edge[] = []; + + allEdges.forEach(edge => { + if (edge.source === selectedNodeId || edge.target === selectedNodeId) { + focusedEdges.push(edge); + connectedNodeIds.add(edge.source); + connectedNodeIds.add(edge.target); + } + }); + + // Get connected nodes + const focusedNodes = allNodes.filter(n => connectedNodeIds.has(n.id)); + + return { nodes: focusedNodes, edges: focusedEdges }; + }, [selectedNodeId, allNodes, allEdges]); +}; + +// Smooth transitions for focus mode +const focusModeStyles = { + transition: 'all 0.3s ease-in-out', + opacity: isInFocus ? 1 : 0.3, + filter: isInFocus ? 'none' : 'blur(2px)', +}; +``` + +### Search Integration + +Search and navigate to specific nodes: + +```typescript +const searchNodes = useCallback(( + nodes: Node[], + query: string +) => { + if (!query.trim()) return []; + + const lowerQuery = query.toLowerCase(); + return nodes.filter(node => + node.data.label.toLowerCase().includes(lowerQuery) || + node.data.description?.toLowerCase().includes(lowerQuery) + ); +}, []); + +const navigateToSearchResult = (nodeId: string) => { + // Expand parent nodes + const nodePath = calculateBreadcrumbPath(nodeId, allNodes); + const parentIds = nodePath.slice(0, -1).map(n => n.id); + + setExpandedIds(prev => new Set([...prev, ...parentIds])); + setSelectedNodeId(nodeId); + + // Fit view to node + fitView({ nodes: [{ id: nodeId }], duration: 800 }); +}; +``` + +## Performance Tools + +### Graph Performance Analyzer + +Create a performance analysis script: + +```javascript +// scripts/graph-analyzer.js +class GraphAnalyzer { + analyzeCode(content, filePath) { + const analysis = { + metrics: { + nodeCount: this.countNodes(content), + edgeCount: this.countEdges(content), + renderTime: this.estimateRenderTime(content), + memoryUsage: this.estimateMemoryUsage(content), + complexity: this.calculateComplexity(content) + }, + issues: [], + optimizations: [], + patterns: this.detectPatterns(content) + }; + + // Detect performance issues + this.detectPerformanceIssues(analysis); + + // Suggest optimizations + this.suggestOptimizations(analysis); + + return analysis; + } + + countNodes(content) { + const nodePatterns = [ + /nodes:\s*\[.*?\]/gs, + /const\s+\w+\s*=\s*\[.*?id:.*?position:/gs + ]; + + let totalCount = 0; + nodePatterns.forEach(pattern => { + const matches = content.match(pattern); + if (matches) { + matches.forEach(match => { + const nodeMatches = match.match(/id:\s*['"`][^'"`]+['"`]/g); + if (nodeMatches) { + totalCount += nodeMatches.length; + } + }); + } + }); + + return totalCount; + } + + estimateRenderTime(content) { + const nodeCount = this.countNodes(content); + const edgeCount = this.countEdges(content); + + // Base render time estimation (ms) + const baseTime = 5; + const nodeTime = nodeCount * 0.1; + const edgeTime = edgeCount * 0.05; + + return baseTime + nodeTime + edgeTime; + } + + detectPerformanceIssues(analysis) { + const { metrics } = analysis; + + if (metrics.nodeCount > 500) { + analysis.issues.push({ + type: 'HIGH_NODE_COUNT', + severity: 'high', + message: `Too many nodes (${metrics.nodeCount}). Consider virtualization.`, + suggestion: 'Implement virtualization or reduce visible nodes' + }); + } + + if (metrics.renderTime > 16) { + analysis.issues.push({ + type: 'SLOW_RENDER', + severity: 'high', + message: `Render time (${metrics.renderTime.toFixed(2)}ms) exceeds 60fps.`, + suggestion: 'Optimize with memoization and incremental rendering' + }); + } + } +} +``` + +## Best Practices + +### Performance Guidelines + +1. **Use React.memo** for node components to prevent unnecessary re-renders +2. **Implement virtualization** for graphs with 1000+ nodes +3. **Debounce layout calculations** during rapid interactions +4. **Use useCallback** for edge creation and manipulation functions +5. **Implement proper TypeScript types** for nodes and edges + +### Memory Management + +```typescript +// Use Map for O(1) lookups instead of array.find +const nodesById = useMemo(() => + new Map(allNodes.map(n => [n.id, n])), [allNodes] +); + +// Cache layout results +const layoutCacheRef = useRef>(new Map()); + +// Proper cleanup in useEffect +useEffect(() => { + return () => { + // Clean up any lingering references + nodesMapRef.current.clear(); + edgesMapRef.current.clear(); + }; +}, []); +``` + +### State Optimization + +```typescript +// Use useRef for objects that shouldn't trigger re-renders +const autoSaveDataRef = useRef({ + nodes: [], + edges: [], + lastSaved: Date.now(), +}); + +// Update properties without breaking reference +const updateAutoSaveData = (newNodes: Node[], newEdges: Edge[]) => { + autoSaveDataRef.current.nodes = newNodes; + autoSaveDataRef.current.edges = newEdges; + autoSaveDataRef.current.lastSaved = Date.now(); +}; +``` + +## Common Problems & Solutions + +### Performance Issues + +- **Problem**: Lag during node expansion +- **Solution**: Implement incremental rendering with change detection + +- **Problem**: Memory usage increases over time +- **Solution**: Proper cleanup in useEffect hooks and use WeakMap for temporary data + +### Layout Conflicts + +- **Problem**: Manual positioning conflicts with auto-layout +- **Solution**: Use controlled positioning state and separate layout modes + +### Rendering Issues + +- **Problem**: Excessive re-renders +- **Solution**: Use memo, useMemo, and useCallback with stable dependencies + +- **Problem**: Slow layout calculations +- **Solution**: Debounce layout calculations and cache results + +## Complete Example + +```typescript +import React, { useState, useCallback, useMemo, useRef } from 'react'; +import ReactFlow, { Node, Edge, useReactFlow } from 'reactflow'; +import dagre from 'dagre'; +import { debounce } from 'lodash'; + +interface GraphState { + nodes: Node[]; + edges: Edge[]; + selectedNodeId: string | null; + expandedNodeIds: Set; + history: GraphState[]; + historyIndex: number; +} + +export default function InteractiveGraph() { + const [state, setState] = useState({ + nodes: [], + edges: [], + selectedNodeId: null, + expandedNodeIds: new Set(), + history: [], + historyIndex: 0, + }); + + const { fitView } = useReactFlow(); + const layoutCacheRef = useRef>(new Map()); + + // Memoized styled edges + const styledEdges = useMemo(() => { + return state.edges.map(edge => ({ + ...edge, + style: { + ...edge.style, + strokeWidth: state.selectedNodeId === edge.source || state.selectedNodeId === edge.target ? 3 : 2, + stroke: state.selectedNodeId === edge.source || state.selectedNodeId === edge.target ? '#3b82f6' : '#94a3b8', + }, + animated: state.selectedNodeId === edge.source || state.selectedNodeId === edge.target, + })); + }, [state.edges, state.selectedNodeId]); + + // Debounced layout calculation + const debouncedLayout = useMemo( + () => debounce((nodes: Node[], edges: Edge[]) => { + const cacheKey = generateLayoutCacheKey(nodes, edges); + + if (layoutCacheRef.current.has(cacheKey)) { + return layoutCacheRef.current.get(cacheKey)!; + } + + const layouted = applyDagreLayout(nodes, edges); + layoutCacheRef.current.set(cacheKey, layouted); + + return layouted; + }, 150), + [] + ); + + const handleNodeClick = useCallback((event: React.MouseEvent, node: Node) => { + setState(prev => ({ + ...prev, + selectedNodeId: node.id, + })); + }, []); + + const handleToggleExpand = useCallback((nodeId: string) => { + setState(prev => { + const newExpanded = new Set(prev.expandedNodeIds); + if (newExpanded.has(nodeId)) { + newExpanded.delete(nodeId); + } else { + newExpanded.add(nodeId); + } + + return { + ...prev, + expandedNodeIds: newExpanded, + }; + }); + }, []); + + return ( + + ); +} +``` + +This comprehensive skill provides everything needed to build production-ready ReactFlow applications with hierarchical navigation, performance optimization, and advanced state management patterns.