Skill: anderskev/react-flow-implementation (version 1.1.1)

--- name: react-flow-implementation description: Implements React Flow node-based UIs correctly using @xyflow/react. Use when building flow charts, diagrams, visual editors, or node-based applications with React. Covers nodes, edges, handles, custom components, state management, and viewport control. --- # React Flow Implementation ## Quick Start ```tsx import { useCallback } from 'react'; import { ReactFlow, useNodesState, useEdgesState, addEdge } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; const initialNodes = [ { id: '1', position: { x: 0, y: 0 }, data: { label: 'Node 1' } }, { id: '2', position: { x: 200, y: 100 }, data: { label: 'Node 2' } }, ]; const initialEdges = [{ id: 'e1-2', source: '1', target: '2' }]; export default function Flow() { const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); const onConnect = useCallback( (connection) => setEdges((eds) => addEdge(connection, eds)), [setEdges] ); return ( <div style={{ width: '100%', height: '100vh' }}> <ReactFlow nodes={nodes} edges={edges} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onConnect={onConnect} fitView /> </div> ); } ``` ## Implementation gates Run these in order; do not skip ahead on “looks fine.” 1. **Styles on the page** — **Pass if** the bundle that renders `<ReactFlow />` imports `@xyflow/react/dist/style.css` (or equivalent CSS pipeline) so nodes/edges are visible and hit-targets match visuals. 2. **`useReactFlow` boundary** — **Pass if** every caller of `useReactFlow()` is a descendant of `<ReactFlowProvider>` that wraps the same tree as `<ReactFlow />` (or you have intentionally split stores and can name both roots). 3. **Stable `nodeTypes` / `edgeTypes`** — **Pass if** those maps are **module-scope constants** or `useMemo` with stable deps—not a new `{ ... }` literal each render in the component that renders `<ReactFlow />`. 4. **Handles match edges** — **Pass if** for nodes with multiple `Handle` `id`s, every edge that must land on a specific handle sets `sourceHandle` / `targetHandle` accordingly (or you accept default handle resolution deliberately). See [ADDITIONAL_COMPONENTS.md](ADDITIONAL_COMPONENTS.md) for MiniMap, Controls, Background, NodeToolbar, and NodeResizer patterns. ## Core Patterns ### TypeScript Types ```typescript import type { Node, Edge, NodeProps, BuiltInNode } from '@xyflow/react'; // Define custom node type with data shape type CustomNode = Node<{ value: number; label: string }, 'custom'>; // Combine with built-in nodes type MyNode = CustomNode | BuiltInNode; type MyEdge = Edge<{ weight?: number }>; // Use throughout app const [nodes, setNodes] = useNodesState<MyNode>(initialNodes); ``` ### Custom Nodes ```tsx import { memo } from 'react'; import { Handle, Position, type NodeProps } from '@xyflow/react'; // Define node type type CounterNode = Node<{ count: number }, 'counter'>; // Always wrap in memo for performance const CounterNode = memo(function CounterNode({ data, isConnectable }: NodeProps<CounterNode>) { return ( <> <Handle type="target" position={Position.Top} isConnectable={isConnectable} /> <div className="counter-node"> Count: {data.count} {/* nodrag prevents dragging when interacting with button */} <button className="nodrag" onClick={() => console.log('clicked')}> Increment </button> </div> <Handle type="source" position={Position.Bottom} isConnectable={isConnectable} /> </> ); }); // Register in nodeTypes (define OUTSIDE component to avoid re-renders) const nodeTypes = { counter: CounterNode }; // Use in ReactFlow <ReactFlow nodeTypes={nodeTypes} ... /> ``` ### Multiple Handles ```tsx // Use handle IDs when a node has multiple handles of same type <Handle type="source" position={Position.Right} id="a" /> <Handle type="source" position={Position.Right} id="b" style={{ top: 20 }} /> // Connect with specific handles const edge = { id: 'e1-2', source: '1', sourceHandle: 'a', target: '2', targetHandle: null }; ``` ### Custom Edges ```tsx import { BaseEdge, EdgeProps, getSmoothStepPath } from '@xyflow/react'; function CustomEdge({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, data }: EdgeProps) { const [edgePath, labelX, labelY] = getSmoothStepPath({ sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition, }); return ( <> <BaseEdge id={id} path={edgePath} /> <text x={labelX} y={labelY} className="edge-label">{data?.label}</text> </> ); } const edgeTypes = { custom: CustomEdge }; ``` ## State Management ### Controlled (Recommended for Production) ```tsx // External state with change handlers const [nodes, setNodes] = useState<Node[]>(initialNodes); const [edges, setEdges] = useState<Edge[]>(initialEdges); const onNodesChange = useCallback( (changes) => setNodes((nds) => applyNodeChanges(changes, nds)), [] ); const onEdgesChange = useCallback( (changes) => setEdges((eds) => applyEdgeChanges(changes, eds)), [] ); <ReactFlow nodes={nodes} edges={edges} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} /> ``` ### Using useReactFlow ```tsx import { useReactFlow, ReactFlowProvider } from '@xyflow/react'; function FlowControls() { const { getNodes, setNodes, addNodes, updateNodeData, getEdges, setEdges, addEdges, fitView, zoomIn, zoomOut, setViewport, deleteElements, toObject, } = useReactFlow(); const addNode = () => { addNodes({ id: `${Date.now()}`, position: { x: 100, y: 100 }, data: { label: 'New' } }); }; return <button onClick={addNode}>Add Node</button>; } // Must wrap in provider when using useReactFlow function App() { return ( <ReactFlowProvider> <Flow /> <FlowControls /> </ReactFlowProvider> ); } ``` ### Updating Node Data ```tsx const { updateNodeData } = useReactFlow(); // Merge with existing data updateNodeData(nodeId, { label: 'Updated' }); // Replace data entirely updateNodeData(nodeId, { newField: 'value' }, { replace: true }); ``` ## Viewport & Fit View ```tsx // Fit on initial render <ReactFlow fitView fitViewOptions={{ padding: 0.2, maxZoom: 1 }} /> // Programmatic control const { fitView, setViewport, getViewport, zoomTo } = useReactFlow(); // Fit to specific nodes fitView({ nodes: [{ id: '1' }, { id: '2' }], duration: 500 }); // Set exact viewport setViewport({ x: 100, y: 100, zoom: 1.5 }, { duration: 300 }); ``` ## Connection Validation ```tsx const isValidConnection = useCallback((connection: Connection) => { // Prevent self-connections if (connection.source === connection.target) return false; // Custom validation logic const sourceNode = getNode(connection.source); const targetNode = getNode(connection.target); return sourceNode?.type !== targetNode?.type; }, []); <ReactFlow isValidConnection={isValidConnection} /> ``` ## Common Props Reference ```tsx <ReactFlow // Core data nodes={nodes} edges={edges} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} // Custom types (define OUTSIDE component) nodeTypes={nodeTypes} edgeTypes={edgeTypes} // Connections onConnect={onConnect} connectionMode={ConnectionMode.Loose} // Allow target-to-target isValidConnection={isValidConnection} // Viewport fitView minZoom={0.1} maxZoom={4} defaultViewport={{ x: 0, y: 0, zoom: 1 }} // Interaction nodesDraggable={true} nodesConnectable={true} elementsSelectable={true} panOnDrag={true} zoomOnScroll={true} // Additional components <MiniMap /> <Controls /> <Background variant={BackgroundVariant.Dots} /> </ReactFlow> ``` ## CSS Classes for Interaction | Class | Effect | |-------|--------| | `nodrag` | Prevent dragging when clicking element | | `nowheel` | Prevent zoom on wheel events | | `nopan` | Prevent panning from element | | `nokey` | Prevent keyboard events (use on inputs) | ## Additional Components See [ADDITIONAL_COMPONENTS.md](ADDITIONAL_COMPONENTS.md) for MiniMap, Controls, Background, NodeToolbar, NodeResizer.

path: ADDITIONAL_COMPONENTS.md
content: # React Flow Additional Components

## MiniMap

```tsx
import { MiniMap } from '@xyflow/react';

<MiniMap
  nodeColor={(node) => {
    switch (node.type) {
      case 'input': return '#6ede87';
      case 'output': return '#ff0072';
      default: return '#eee';
    }
  }}
  nodeStrokeWidth={3}
  zoomable
  pannable
/>
```

## Controls

```tsx
import { Controls } from '@xyflow/react';

<Controls
  showZoom={true}
  showFitView={true}
  showInteractive={true}
  position="bottom-left"
/>
```

## Background

```tsx
import { Background, BackgroundVariant } from '@xyflow/react';

// Dots pattern
<Background variant={BackgroundVariant.Dots} gap={16} size={1} />

// Lines pattern
<Background variant={BackgroundVariant.Lines} gap={24} />

// Cross pattern
<Background variant={BackgroundVariant.Cross} />

// Custom color
<Background bgColor="#1a1a1a" color="#444" />
```

## NodeToolbar

```tsx
import { NodeToolbar, Position } from '@xyflow/react';

function CustomNode({ id, selected }: NodeProps) {
  return (
    <>
      <NodeToolbar
        isVisible={selected}
        position={Position.Top}
      >
        <button onClick={() => console.log('delete', id)}>Delete</button>
        <button>Edit</button>
      </NodeToolbar>
      <div>Node Content</div>
    </>
  );
}
```

## NodeResizer

```tsx
import { NodeResizer } from '@xyflow/react';

function ResizableNode({ selected }: NodeProps) {
  return (
    <>
      <NodeResizer
        isVisible={selected}
        minWidth={100}
        minHeight={50}
        handleStyle={{ width: 8, height: 8 }}
      />
      <div style={{ padding: 10 }}>
        Resize me
      </div>
    </>
  );
}
```

## EdgeToolbar (for custom edges)

```tsx
import { EdgeToolbar } from '@xyflow/react';

function CustomEdge({ id, selected, ...props }: EdgeProps) {
  const [path, labelX, labelY] = getSmoothStepPath(props);

  return (
    <>
      <BaseEdge path={path} />
      <EdgeToolbar x={labelX} y={labelY} isVisible={selected}>
        <button>Edit</button>
      </EdgeToolbar>
    </>
  );
}
```

## Panel

```tsx
import { Panel } from '@xyflow/react';

// Positions: top-left, top-center, top-right, bottom-left, bottom-center, bottom-right
<ReactFlow ...>
  <Panel position="top-right">
    <button onClick={onSave}>Save</button>
    <button onClick={onRestore}>Restore</button>
  </Panel>
</ReactFlow>
```


path: EDGE_PATHS.md
content: # Edge Path Utilities

React Flow provides utilities for generating SVG paths for custom edges.

## Available Path Functions

### getBezierPath (Default)
```tsx
import { getBezierPath, Position } from '@xyflow/react';

const [path, labelX, labelY, offsetX, offsetY] = getBezierPath({
  sourceX: 0,
  sourceY: 0,
  sourcePosition: Position.Right,
  targetX: 200,
  targetY: 100,
  targetPosition: Position.Left,
  curvature: 0.25,  // optional, default 0.25
});
```

### getSmoothStepPath
```tsx
import { getSmoothStepPath } from '@xyflow/react';

const [path, labelX, labelY] = getSmoothStepPath({
  sourceX, sourceY, sourcePosition,
  targetX, targetY, targetPosition,
  borderRadius: 5,      // Corner rounding
  offset: 20,           // Distance from handle before first bend
  stepPosition: 0.5,    // 0-1, where bend occurs (0=source, 1=target)
});
```

### getStraightPath
```tsx
import { getStraightPath } from '@xyflow/react';

const [path, labelX, labelY] = getStraightPath({
  sourceX, sourceY,
  targetX, targetY,
});
```

### getSimpleBezierPath
```tsx
import { getSimpleBezierPath } from '@xyflow/react';

const [path, labelX, labelY] = getSimpleBezierPath({
  sourceX, sourceY, sourcePosition,
  targetX, targetY, targetPosition,
});
```

## Custom Edge Example

```tsx
import { BaseEdge, EdgeProps, getSmoothStepPath, EdgeLabelRenderer } from '@xyflow/react';

function CustomEdge({
  id, sourceX, sourceY, targetX, targetY,
  sourcePosition, targetPosition, data, style
}: EdgeProps) {
  const [edgePath, labelX, labelY] = getSmoothStepPath({
    sourceX, sourceY, sourcePosition,
    targetX, targetY, targetPosition,
  });

  return (
    <>
      <BaseEdge id={id} path={edgePath} style={style} />
      <EdgeLabelRenderer>
        <div
          style={{
            position: 'absolute',
            transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
            pointerEvents: 'all',
          }}
          className="nodrag nopan"
        >
          <button onClick={() => console.log('edge clicked', id)}>
            {data?.label}
          </button>
        </div>
      </EdgeLabelRenderer>
    </>
  );
}
```

## Edge Markers

```tsx
// Built-in markers
const edge = {
  id: 'e1-2',
  source: '1',
  target: '2',
  markerEnd: MarkerType.ArrowClosed,
  // or custom:
  markerEnd: {
    type: MarkerType.Arrow,
    color: '#f00',
    width: 20,
    height: 20,
  },
};
```


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)