Unnamed Skill

XYFlow integration patterns for ChainGraph visual flow editor. Use when working on node rendering, edge rendering, drag-and-drop, selection handling, anchors, handles, or any XYFlow-related UI code. Covers custom nodes/edges, performance optimization, handle positioning. Triggers: xyflow, reactflow, node rendering, edge rendering, handle, anchor, drag drop, selection, viewport, canvas, flow editor UI.

$ 설치

git clone https://github.com/chaingraphlabs/chaingraph /tmp/chaingraph && cp -r /tmp/chaingraph/.claude/skills/xyflow-patterns ~/.claude/skills/chaingraph

// tip: Run this command in your terminal to install the skill


name: xyflow-patterns description: XYFlow integration patterns for ChainGraph visual flow editor. Use when working on node rendering, edge rendering, drag-and-drop, selection handling, anchors, handles, or any XYFlow-related UI code. Covers custom nodes/edges, performance optimization, handle positioning. Triggers: xyflow, reactflow, node rendering, edge rendering, handle, anchor, drag drop, selection, viewport, canvas, flow editor UI.

XYFlow Patterns for ChainGraph

This skill covers XYFlow (React Flow) integration patterns used in the ChainGraph visual flow editor.

XYFlow Overview

Library: @xyflow/react (React Flow v12+) Purpose: Canvas-based flow editor with nodes, edges, zoom, pan ChainGraph Integration: apps/chaingraph-frontend/src/components/flow/Flow.tsx

Architecture

┌────────────────────────────────────────────────────────────┐
│                    Flow.tsx (Main Component)                │
│  ├─ ReactFlow                                               │
│  │   ├─ nodes (from useXYFlowNodes())                       │
│  │   ├─ edges (from useXYFlowEdges())                       │
│  │   ├─ nodeTypes (chaingraphNode, groupNode, anchorNode)   │
│  │   ├─ edgeTypes (flow, animated, default)                 │
│  │   └─ callbacks (onNodesChange, onEdgesChange, ...)       │
│  ├─ Background                                              │
│  ├─ StyledControls                                          │
│  └─ Custom UI Overlays (ContextMenu, ControlPanel)          │
└────────────────────────────────────────────────────────────┘

Node Types

ChainGraph defines 3 custom node types:

File: apps/chaingraph-frontend/src/components/flow/Flow.tsx:134-138

const nodeTypes = useMemo(() => ({
  chaingraphNode: ChaingraphNodeOptimized,  // Main computational node
  groupNode: memo(GroupNode),               // Container for grouping
  anchorNode: memo(AnchorNode),             // Edge anchor waypoints
}), [])

ChaingraphNodeOptimized

File: apps/chaingraph-frontend/src/components/flow/nodes/ChaingraphNode/ChaingraphNodeOptimized.tsx

The main node component with heavy optimization via memoization:

const ChaingraphNodeOptimized = memo(
  (props: NodeProps<ChaingraphNode>) => <ChaingraphNodeComponent {...props} />,
  (prevProps, nextProps) => {
    // Custom comparison for performance
    // Returns false (re-render) when id, selected, version, width, or height change
    return true // (no re-render) when everything matches
  },
)

ChaingraphNode Component

File: apps/chaingraph-frontend/src/components/flow/nodes/ChaingraphNode/ChaingraphNode.tsx

Uses single consolidated render data subscription:

function ChaingraphNodeComponent({ data, selected, id }: NodeProps<ChaingraphNode>) {
  // ✅ Single subscription for ALL render data (replaces 10 hooks)
  const renderData = useXYFlowNodeRenderData(id)

  // ✅ Flow metadata (keep separate - rarely changes, used for handlers)
  const activeFlow = useUnit($activeFlowMetadata)

  // ✅ Flow loaded (keep separate - simple guard)
  const isFlowLoaded = useUnit($isFlowLoaded)

  // ... component rendering
}

Performance Result: 97% fewer re-renders during drag operations (from 13 subscriptions to 4).


Performance Optimization

Consolidated Render Data Store

Store: $xyflowNodeRenderMap (NOT $xyflowNodeRenderData) File: apps/chaingraph-frontend/src/store/xyflow/stores/node-render-data.ts Hook: useXYFlowNodeRenderData(nodeId)

// Hook usage (apps/chaingraph-frontend/src/store/xyflow/hooks/useXYFlowNodeRenderData.ts)
const renderData = useXYFlowNodeRenderData(nodeId)

XYFlowNodeRenderData Interface

File: apps/chaingraph-frontend/src/store/xyflow/types.ts:48-114

export interface XYFlowNodeRenderData {
  // Core identity
  nodeId: string
  version: number

  // Port ID arrays (pre-computed - no iteration in components!)
  inputPortIds: string[]
  outputPortIds: string[]
  passthroughPortIds: string[]

  // Specific system ports (pre-computed)
  flowInputPortId: string | null
  flowOutputPortId: string | null
  errorPortId: string | null
  errorMessagePortId: string | null

  // Metadata
  title: string
  status: 'idle' | 'running' | 'completed' | 'failed' | 'skipped'

  // Position & dimensions
  position: Position
  dimensions: { width: number, height: number }

  // Visual properties
  nodeType: 'chaingraphNode' | 'groupNode'
  categoryMetadata: CategoryMetadata
  zIndex: number

  // State flags
  isSelected: boolean
  isHidden: boolean
  isDraggable: boolean
  parentNodeId: string | undefined

  // Execution state
  executionStyle: string | undefined
  executionStatus: NodeExecutionStatus
  executionNode: ExecutionNodeData | null

  // Interaction state
  isHighlighted: boolean
  hasAnyHighlights: boolean
  pulseState: PulseState
  dropFeedback: DropFeedback | null

  // Debug state
  hasBreakpoint: boolean
  debugMode: boolean
}

8-Wire Delta Update System

The store uses 8 wires for surgical delta updates instead of full recalculation:

  1. Position updates - High frequency (60fps during drag)
  2. Node data changes - Version, dimensions, selection
  3. Execution state - Execution events
  4. Highlight changes - User highlights
  5. Pulse state - Animation (200ms intervals)
  6. Drop feedback - Drag operations
  7. Layer depth - Parent structure changes
  8. Category metadata - Theme changes

Edge Types

File: apps/chaingraph-frontend/src/components/flow/edges/index.ts

export const edgeTypes = {
  animated: AnimatedEdge,
  flow: FlowEdge,
  default: AnimatedEdge, // Fallback for edges without explicit type
} satisfies EdgeTypes

Note: Edge type keys are flow, animated, default - NOT flowEdge, animatedEdge.

FlowEdge Component

File: apps/chaingraph-frontend/src/components/flow/edges/FlowEdge.tsx

Features:

  • Catmull-Rom splines via catmullRomToBezierPath()
  • Ghost anchors for adding new waypoints
  • Selection highlighting
  • Hover state feedback
  • Animated particle effects (when data.animated = true)
export const FlowEdge = memo(({
  id, sourceX, sourceY, targetX, targetY,
  sourcePosition, targetPosition, style, data,
}: EdgeProps) => {
  // Anchor support
  const selectedEdgeId = useUnit($selectedEdgeId)
  const isSelected = selectedEdgeId === id

  // Get anchor positions from anchor nodes store
  // PROTOTYPE: Anchors are now XYFlow nodes, positions come from their node positions
  const anchorPositions = useAnchorNodePositions(edgeId)

  // Path calculation with Catmull-Rom splines
  const pathData = useMemo(() => {
    return catmullRomToBezierPath(source, target, anchorPositions, sourcePosition, targetPosition)
  }, [source, target, anchorPositions, sourcePosition, targetPosition])

  // Ghost anchors (only when selected)
  const ghostAnchors = useMemo(() => {
    if (!isSelected) return []
    return calculateGhostAnchors(source, target, anchorPositions, sourcePosition, targetPosition)
  }, [isSelected, source, target, anchorPositions, sourcePosition, targetPosition])

  // ...
})

Anchor System

Key Insight: Anchors are now XYFlow nodes (anchorNode type), NOT SVG circles rendered inside edges.

Architecture

User clicks ghost anchor
    ↓
addAnchorNode event fires
    ↓
$anchorNodes store updates
    ↓
$anchorXYFlowNodes derived store creates XYFlow Node
    ↓
XYFlow handles drag/selection natively
    ↓
FlowEdge queries anchor positions for path calculation
    ↓
Changes sync to backend in EdgeMetadata.anchors[] format

AnchorNodeState

File: apps/chaingraph-frontend/src/store/edges/anchor-nodes.ts:46-56

export interface AnchorNodeState {
  id: string
  edgeId: string
  x: number // Flow position (top-left of node, not center)
  y: number
  index: number
  color?: string
  parentNodeId?: string // For XYFlow native parenting
  selected?: boolean
  version: number // Increments on any change to force XYFlow re-render
}

EdgeAnchor Interface (Backend)

File: packages/chaingraph-types/src/edge/types.ts:27-40

export interface EdgeAnchor {
  /** Unique identifier */
  id: string
  /** X coordinate (absolute if no parent, relative if parentNodeId is set) */
  x: number
  /** Y coordinate (absolute if no parent, relative if parentNodeId is set) */
  y: number
  /** Order index in path (0 = closest to source) */
  index: number
  /** Parent group node ID (if anchor is child of a group) */
  parentNodeId?: string
  /** Selection state (set by backend during paste operations) */
  selected?: boolean
}

Anchor Events

// Add anchor (from ghost anchor click)
export const addAnchorNode = edgesDomain.createEvent<{
  edgeId: string
  x: number
  y: number
  index: number
  color?: string
}>()

// Remove anchor (double-click or Delete key)
export const removeAnchorNode = edgesDomain.createEvent<{
  anchorNodeId: string
  edgeId?: string
}>()

// Update position (from XYFlow drag)
export const updateAnchorNodePosition = edgesDomain.createEvent<{
  anchorNodeId: string
  x: number
  y: number
}>()

Ghost Anchors

Ghost anchors are SVG visual hints that appear when an edge is selected:

// FlowEdge.tsx:268-272
const ghostAnchors = useMemo(() => {
  if (!isSelected) return []
  return calculateGhostAnchors(source, target, anchorPositions, sourcePosition, targetPosition)
}, [isSelected, source, target, anchorPositions, sourcePosition, targetPosition])

// Click handler creates real anchor node
const handleGhostClick = useCallback((insertIndex: number, x: number, y: number) => {
  addAnchorNode({
    edgeId,
    x,
    y,
    index: insertIndex,
    color: stroke,
  })
}, [edgeId, stroke])

Handle Positioning

Handle positioning is delegated to XYFlow's automatic layout system.

File: apps/chaingraph-frontend/src/components/flow/nodes/ChaingraphNode/ports/ui/PortHandle.tsx

const position = direction === 'input'
  ? Position.Left
  : Position.Right

<Handle
  type={direction === 'input' ? 'target' : 'source'}
  position={position}
  id={portId}
/>

Note: ChainGraph does NOT use custom calculateHandlePosition() functions. Vertical handle distribution is managed by the component layout, not explicit Y positioning.


Custom Hooks

Flow Interaction Hooks (18 hooks)

Location: apps/chaingraph-frontend/src/components/flow/hooks/

HookPurpose
useBoxSelectionBlender-style box selection with B key
useCanvasHoverCanvas hover detection for hotkeys
useConnectionHandlingConnection creation with cycle detection
useEdgeAnchorKeyboardKeyboard shortcuts for anchor management
useEdgeChangesEdge removal and selection handling
useEdgeKeyboardShortcutsEdge-related keyboard shortcuts
useEdgeReconnectionEdge reconnection (onReconnectStart/onReconnect/onReconnectEnd)
useFlowCallbacksOrchestrates all flow interaction callbacks
useFlowCopyPasteCopy/paste and export/import operations
useFlowUtilsUtility functions (NOT a React hook - exports pure functions)
useGrabModeBlender-style grab mode with G key
useKeyboardShortcutsUnified shortcuts (Ctrl+C, Ctrl+V, Shift+D, A, F, X)
useNodeChangesNode position, selection, and parent updates
useNodeDragHandlingNode drag with parent/group management
useNodeDropNode drop handling with position calculation
useNodeSchemaDropEventsNode schema drop detection via event emitter
useNodeSelectionNode selection utilities (helper functions)
useSelectionHotkeysSelection-related hotkeys

XYFlow Data Hooks

Location: apps/chaingraph-frontend/src/store/xyflow/hooks/

HookPurpose
useXYFlowNodeRenderDataSingle subscription for all node render data
useXYFlowNodeBodyPortsBody port IDs for node body rendering
useXYFlowNodeErrorPortsError port IDs for error section
useXYFlowNodeFlowPortsFlow port IDs (input/output)
useXYFlowNodeHeaderDataHeader data (title, category, etc.)

Store Data Hooks

Location: apps/chaingraph-frontend/src/store/*/hooks/

HookPurpose
useXYFlowNodesXYFlow-compatible nodes from Effector stores
useXYFlowEdgesXYFlow-compatible edges from Effector stores

ReactFlow Configuration

File: apps/chaingraph-frontend/src/components/flow/Flow.tsx:303-356

<ReactFlow
  nodes={nodes}
  nodeTypes={nodeTypes}
  edges={edges}
  edgeTypes={edgeTypes}

  // Callbacks
  onNodesChange={onNodesChange}
  onEdgesChange={onEdgesChange}
  onConnect={onConnect}
  onConnectStart={...}
  onConnectEnd={...}
  onNodeClick={handleNodeClick}
  onEdgeClick={handleEdgeClick}
  onPaneClick={handlePaneClick}
  onReconnect={onReconnect}
  onReconnectStart={onReconnectStart}
  onReconnectEnd={onReconnectEnd}
  onNodeDrag={onNodeDrag}
  onNodeDragStart={onNodeDragStart}
  onNodeDragStop={onNodeDragStop}
  onSelectionEnd={onSelectionEnd}
  onViewportChange={onViewportChange}

  // Selection & Pan
  panOnScroll={true}
  panOnDrag={panOnDrag}           // From useBoxSelection()
  selectionOnDrag={selectionOnDrag} // From useBoxSelection()
  selectionMode={selectionMode}   // From useBoxSelection()

  // Features
  zoomOnDoubleClick={true}
  connectOnClick={true}
  deleteKeyCode={['Delete', 'Backspace']}
  fitView={true}
  preventScrolling

  // Zoom limits
  minZoom={0.05}
  maxZoom={2}

  // Drag settings
  nodeDragThreshold={1}
  nodesDraggable={!isGrabMode}

  // Viewport
  defaultViewport={{ x: 0, y: 0, zoom: 0.2 }}
  defaultEdgeOptions={{ animated: true }}

  className="bg-background"
>
  <Background />
  <NodeInternalsSync />
  <StyledControls position="bottom-right" />
  {activeFlowId && <FlowControlPanel />}
</ReactFlow>

Key Files

FilePurpose
components/flow/Flow.tsxMain XYFlow container
components/flow/nodes/ChaingraphNode/ChaingraphNodeOptimized.tsxOptimized node wrapper
components/flow/nodes/ChaingraphNode/ChaingraphNode.tsxMain node component
components/flow/nodes/AnchorNode/AnchorNode.tsxAnchor node component
components/flow/edges/FlowEdge.tsxCustom edge with anchors
components/flow/edges/index.tsEdge type registration
store/xyflow/types.tsXYFlowNodeRenderData interface
store/xyflow/stores/node-render-data.ts$xyflowNodeRenderMap store
store/xyflow/hooks/useXYFlowNodeRenderData.tsRender data hook
store/nodes/hooks/useXYFlowNodes.tsNode data transformation
store/edges/hooks/useXYFlowEdges.tsEdge data transformation
store/edges/anchor-nodes.tsAnchor node store and events
components/flow/hooks/18 interaction hooks

Common Patterns

Adding a Custom Node Type

// 1. Create node component
function MyCustomNode({ id, data }: NodeProps<MyData>) {
  return (
    <div className="my-custom-node">
      <Handle type="target" position={Position.Left} />
      {data.label}
      <Handle type="source" position={Position.Right} />
    </div>
  )
}

// 2. Register in nodeTypes (Flow.tsx)
const nodeTypes = useMemo(() => ({
  chaingraphNode: ChaingraphNodeOptimized,
  groupNode: memo(GroupNode),
  anchorNode: memo(AnchorNode),
  myCustomNode: memo(MyCustomNode),  // Add here
}), [])

// 3. Use in node data
addNode({
  id: 'node-1',
  type: 'myCustomNode',
  position: { x: 100, y: 100 },
  data: { label: 'Custom' },
})

Custom Edge Styling

function StyledEdge({ id, ...props }: EdgeProps) {
  const selectedEdgeId = useUnit($selectedEdgeId)
  const isActive = selectedEdgeId === id

  return (
    <path
      {...props}
      style={{
        stroke: isActive ? '#3b82f6' : '#6b7280',
        strokeWidth: isActive ? 3 : 2,
      }}
    />
  )
}

Related Skills

  • frontend-architecture - Overall frontend structure
  • effector-patterns - Store patterns used
  • subscription-sync - Real-time node/edge updates
  • optimistic-updates - Position interpolation
  • chaingraph-concepts - Node/edge domain concepts