Back to list
chaingraphlabs

xyflow-patterns

by chaingraphlabs

ChainGraph is a source available, type-safe flow-based programming framework for building AI LLM agents with complex logic and dynamic computational graphs.

14🍴 2📅 Jan 22, 2026

SKILL.md


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,
      }}
    />
  )
}

  • 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

Score

Total Score

75/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

+10
人気

GitHub Stars 100以上

0/15
最近の活動

1ヶ月以内に更新

+10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

+5

Reviews

💬

Reviews coming soon