--- 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..." risk: unknown source: community --- # 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. ## When to Use This skill is applicable to execute the workflow or actions described in the overview.