Técnico10 min

Construyendo un Editor Visual con React Flow

Un vistazo técnico a cómo construimos nuestro editor drag-and-drop, los desafíos de la customización de nodos y la lógica de conexiones.

LC

Luis Chen

CPO & Co-founder

El Corazón de InfraUX: Nuestro Editor Visual

Si hay una característica que define a InfraUX, es nuestro editor visual. Construir un editor drag-and-drop que sea intuitivo, performante y poderoso ha sido uno de nuestros mayores desafíos técnicos. Esta es la historia de cómo lo logramos.

¿Por Qué React Flow?

Evaluamos múltiples librerías para construir nuestro editor:

LibreríaProsContrasDecisión
D3.jsMáximo controlRequiere construir todo desde cero
Cytoscape.jsPotente para grafosOrientado a grafos científicos
JointJSBuenas característicasLicencia comercial costosa
React FlowModerno, bien mantenidoCurva de aprendizaje

React Flow ganó por su balance entre flexibilidad y facilidad de uso, además de una comunidad activa y excelente documentación.

Arquitectura del Editor

Nuestro editor está compuesto por varias capas:

1// components/Editor/index.tsx 2import ReactFlow, { 3 Background, 4 Controls, 5 MiniMap, 6 ReactFlowProvider 7} from 'reactflow'; 8 9export function InfraUXEditor() { 10 return ( 11 <ReactFlowProvider> 12 <div className="h-full w-full"> 13 <ReactFlow 14 nodes={nodes} 15 edges={edges} 16 onNodesChange={onNodesChange} 17 onEdgesChange={onEdgesChange} 18 onConnect={onConnect} 19 nodeTypes={nodeTypes} 20 edgeTypes={edgeTypes} 21 connectionLineComponent={CustomConnectionLine} 22 onDrop={onDrop} 23 onDragOver={onDragOver} 24 snapToGrid 25 snapGrid={[15, 15]} 26 > 27 <Background variant="dots" gap={12} size={1} /> 28 <Controls /> 29 <MiniMap /> 30 <Panel position="top-left"> 31 <Toolbar /> 32 </Panel> 33 </ReactFlow> 34 </div> 35 </ReactFlowProvider> 36 ); 37}

Customización de Nodos

Cada servicio cloud tiene su propio nodo customizado con propiedades específicas:

1// nodes/EC2Node.tsx 2export function EC2Node({ data, selected }: NodeProps) { 3 const [isEditing, setIsEditing] = useState(false); 4 5 return ( 6 <div className={`node-wrapper ${selected ? 'selected' : ''}`}> 7 <Handle type="target" position={Position.Top} /> 8 9 <div className="node-header"> 10 <EC2Icon className="w-6 h-6" /> 11 <span>EC2 Instance</span> 12 </div> 13 14 <div className="node-body"> 15 {isEditing ? ( 16 <InstanceTypeSelector 17 value={data.instanceType} 18 onChange={(type) => updateNodeData({ instanceType: type })} 19 /> 20 ) : ( 21 <div onClick={() => setIsEditing(true)}> 22 {data.instanceType || 't3.micro'} 23 </div> 24 )} 25 26 <div className="node-stats"> 27 <span>{data.vcpus || 2} vCPUs</span> 28 <span>{data.memory || 1} GB RAM</span> 29 </div> 30 </div> 31 32 <Handle type="source" position={Position.Bottom} /> 33 </div> 34 ); 35} 36 37// Registro de tipos de nodos 38const nodeTypes = { 39 ec2Instance: EC2Node, 40 rdsDatabase: RDSNode, 41 s3Bucket: S3Node, 42 lambda: LambdaNode, 43 vpc: VPCNode, 44 // ... más de 50 tipos de nodos 45};

Sistema de Drag & Drop

Implementamos un sistema de drag & drop intuitivo desde la paleta de componentes:

1// components/ComponentPalette.tsx 2export function ComponentPalette() { 3 const onDragStart = (event: DragEvent, nodeType: string) => { 4 event.dataTransfer.setData('application/reactflow', nodeType); 5 event.dataTransfer.effectAllowed = 'move'; 6 }; 7 8 return ( 9 <div className="component-palette"> 10 {Object.entries(AWS_SERVICES).map(([key, service]) => ( 11 <div 12 key={key} 13 className="palette-item" 14 draggable 15 onDragStart={(e) => onDragStart(e, service.nodeType)} 16 > 17 <service.icon className="w-8 h-8" /> 18 <span>{service.name}</span> 19 </div> 20 ))} 21 </div> 22 ); 23}

Validación de Conexiones en Tiempo Real

No todas las conexiones son válidas. Implementamos reglas complejas para validar conexiones:

1// utils/connectionRules.ts 2export const connectionRules = { 3 ec2Instance: { 4 canConnectTo: ['securityGroup', 'elasticIp', 'loadBalancer', 'vpc'], 5 canReceiveFrom: ['loadBalancer', 'autoScalingGroup'], 6 }, 7 rdsDatabase: { 8 canConnectTo: ['securityGroup', 'vpc', 'subnetGroup'], 9 canReceiveFrom: ['ec2Instance', 'lambda'], 10 validation: (source, target) => { 11 // RDS debe estar en la misma VPC que EC2 12 return source.data.vpcId === target.data.vpcId; 13 } 14 }, 15 // ... más reglas 16}; 17 18// Hook para validar conexiones 19const isValidConnection = useCallback((connection: Connection) => { 20 const sourceNode = nodes.find(n => n.id === connection.source); 21 const targetNode = nodes.find(n => n.id === connection.target); 22 23 if (!sourceNode || !targetNode) return false; 24 25 const rules = connectionRules[sourceNode.type]; 26 if (!rules) return false; 27 28 // Verificar si el tipo de target es permitido 29 if (!rules.canConnectTo.includes(targetNode.type)) { 30 showError(`${sourceNode.type} no puede conectarse a ${targetNode.type}`); 31 return false; 32 } 33 34 // Validación adicional si existe 35 if (rules.validation) { 36 return rules.validation(sourceNode, targetNode); 37 } 38 39 return true; 40}, [nodes]);

Optimizaciones de Performance

Con diagramas complejos (100+ nodos), el performance es crítico:

1. Virtualización de Nodos

1// Solo renderizar nodos visibles 2const visibleNodes = useMemo(() => { 3 return nodes.filter(node => { 4 const { x, y } = node.position; 5 return ( 6 x > viewport.x - 200 && 7 x < viewport.x + viewport.width + 200 && 8 y > viewport.y - 200 && 9 y < viewport.y + viewport.height + 200 10 ); 11 }); 12}, [nodes, viewport]);

2. Debouncing de Updates

1// Debounce actualizaciones para evitar re-renders excesivos 2const debouncedUpdateNode = useMemo( 3 () => debounce((nodeId: string, data: any) => { 4 setNodes((nds) => 5 nds.map((node) => 6 node.id === nodeId ? { ...node, data: { ...node.data, ...data } } : node 7 ) 8 ); 9 }, 100), 10 [] 11);

3. Memoización Agresiva

1// Memoizar componentes pesados 2const MemoizedNode = memo(({ data, selected }) => { 3 return <CustomNode data={data} selected={selected} />; 4}, (prevProps, nextProps) => { 5 // Solo re-render si cambian propiedades relevantes 6 return ( 7 prevProps.selected === nextProps.selected && 8 prevProps.data.lastModified === nextProps.data.lastModified 9 ); 10});

Features Avanzadas

1. Auto-Layout

Implementamos auto-layout usando dagre para organizar automáticamente los nodos:

1import dagre from 'dagre'; 2 3export function autoLayout(nodes: Node[], edges: Edge[]) { 4 const dagreGraph = new dagre.graphlib.Graph(); 5 dagreGraph.setDefaultEdgeLabel(() => ({})); 6 dagreGraph.setGraph({ rankdir: 'TB', nodesep: 100, ranksep: 100 }); 7 8 nodes.forEach((node) => { 9 dagreGraph.setNode(node.id, { width: 180, height: 100 }); 10 }); 11 12 edges.forEach((edge) => { 13 dagreGraph.setEdge(edge.source, edge.target); 14 }); 15 16 dagre.layout(dagreGraph); 17 18 return nodes.map((node) => { 19 const nodeWithPosition = dagreGraph.node(node.id); 20 return { 21 ...node, 22 position: { 23 x: nodeWithPosition.x - 90, 24 y: nodeWithPosition.y - 50, 25 }, 26 }; 27 }); 28}

2. Undo/Redo

Sistema completo de undo/redo para todas las acciones:

1const { undo, redo, canUndo, canRedo } = useUndoRedo({ 2 nodes, 3 edges, 4 maxHistorySize: 50, 5}); 6 7// Keyboard shortcuts 8useHotkeys('cmd+z', () => canUndo && undo()); 9useHotkeys('cmd+shift+z', () => canRedo && redo());

Lecciones Aprendidas

<div class="info-box"> 💡 **La UX es todo:** Invertimos mucho tiempo en hacer el editor intuitivo. Pequeños detalles como el snap-to-grid y el feedback visual hacen una gran diferencia. </div> <div class="warning-box"> ⚠️ **Performance desde el día 1:** Es más fácil optimizar desde el inicio que después. Considera virtualización y memoización desde el principio. </div> <div class="success-box"> ✅ **Feedback visual constante:** Los usuarios necesitan saber qué está pasando siempre. Indicadores de estado, tooltips y animaciones suaves son esenciales. </div>

Métricas de Performance

MétricaValorObjetivo
Tiempo de renderizado inicial120ms< 200ms
FPS durante drag58 fps> 30 fps
Memoria con 100 nodos45MB< 100MB
Tiempo de auto-layout230ms< 500ms

El Futuro del Editor

Estamos trabajando en features aún más avanzadas:

  • AI-Assisted Design: Sugerencias inteligentes mientras diseñas
  • 3D View: Visualización tridimensional de la arquitectura
  • Time Travel: Ver cómo evolucionó el diagrama en el tiempo
  • Smart Templates: Templates que se adaptan a tus necesidades

Construir este editor ha sido un viaje increíble. Cada día aprendemos algo nuevo de nuestros usuarios y mejoramos la experiencia. El editor visual no es solo una feature de InfraUX, es la esencia de lo que somos: hacer la infraestructura visual, intuitiva y colaborativa.

#reactflow#frontend#editor#dragdrop