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,
groupNode: memo(GroupNode),
anchorNode: memo(AnchorNode),
}), [])
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) => {
return true
},
)
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>) {
const renderData = useXYFlowNodeRenderData(id)
const activeFlow = useUnit($activeFlowMetadata)
const isFlowLoaded = useUnit($isFlowLoaded)
}
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)
const renderData = useXYFlowNodeRenderData(nodeId)
XYFlowNodeRenderData Interface
File: apps/chaingraph-frontend/src/store/xyflow/types.ts:48-114
export interface XYFlowNodeRenderData {
nodeId: string
version: number
inputPortIds: string[]
outputPortIds: string[]
passthroughPortIds: string[]
flowInputPortId: string | null
flowOutputPortId: string | null
errorPortId: string | null
errorMessagePortId: string | null
title: string
status: 'idle' | 'running' | 'completed' | 'failed' | 'skipped'
position: Position
dimensions: { width: number, height: number }
nodeType: 'chaingraphNode' | 'groupNode'
categoryMetadata: CategoryMetadata
zIndex: number
isSelected: boolean
isHidden: boolean
isDraggable: boolean
parentNodeId: string | undefined
executionStyle: string | undefined
executionStatus: NodeExecutionStatus
executionNode: ExecutionNodeData | null
isHighlighted: boolean
hasAnyHighlights: boolean
pulseState: PulseState
dropFeedback: DropFeedback | null
hasBreakpoint: boolean
debugMode: boolean
}
8-Wire Delta Update System
The store uses 8 wires for surgical delta updates instead of full recalculation:
- Position updates - High frequency (60fps during drag)
- Node data changes - Version, dimensions, selection
- Execution state - Execution events
- Highlight changes - User highlights
- Pulse state - Animation (200ms intervals)
- Drop feedback - Drag operations
- Layer depth - Parent structure changes
- 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,
} 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) => {
const selectedEdgeId = useUnit($selectedEdgeId)
const isSelected = selectedEdgeId === id
const anchorPositions = useAnchorNodePositions(edgeId)
const pathData = useMemo(() => {
return catmullRomToBezierPath(source, target, anchorPositions, sourcePosition, targetPosition)
}, [source, target, anchorPositions, sourcePosition, targetPosition])
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
y: number
index: number
color?: string
parentNodeId?: string
selected?: boolean
version: number
}
EdgeAnchor Interface (Backend)
File: packages/chaingraph-types/src/edge/types.ts:27-40
export interface EdgeAnchor {
id: string
x: number
y: number
index: number
parentNodeId?: string
selected?: boolean
}
Anchor Events
export const addAnchorNode = edgesDomain.createEvent<{
edgeId: string
x: number
y: number
index: number
color?: string
}>()
export const removeAnchorNode = edgesDomain.createEvent<{
anchorNodeId: string
edgeId?: string
}>()
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:
const ghostAnchors = useMemo(() => {
if (!isSelected) return []
return calculateGhostAnchors(source, target, anchorPositions, sourcePosition, targetPosition)
}, [isSelected, source, target, anchorPositions, sourcePosition, targetPosition])
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/
| Hook | Purpose |
|---|
useBoxSelection | Blender-style box selection with B key |
useCanvasHover | Canvas hover detection for hotkeys |
useConnectionHandling | Connection creation with cycle detection |
useEdgeAnchorKeyboard | Keyboard shortcuts for anchor management |
useEdgeChanges | Edge removal and selection handling |
useEdgeKeyboardShortcuts | Edge-related keyboard shortcuts |
useEdgeReconnection | Edge reconnection (onReconnectStart/onReconnect/onReconnectEnd) |
useFlowCallbacks | Orchestrates all flow interaction callbacks |
useFlowCopyPaste | Copy/paste and export/import operations |
useFlowUtils | Utility functions (NOT a React hook - exports pure functions) |
useGrabMode | Blender-style grab mode with G key |
useKeyboardShortcuts | Unified shortcuts (Ctrl+C, Ctrl+V, Shift+D, A, F, X) |
useNodeChanges | Node position, selection, and parent updates |
useNodeDragHandling | Node drag with parent/group management |
useNodeDrop | Node drop handling with position calculation |
useNodeSchemaDropEvents | Node schema drop detection via event emitter |
useNodeSelection | Node selection utilities (helper functions) |
useSelectionHotkeys | Selection-related hotkeys |
XYFlow Data Hooks
Location: apps/chaingraph-frontend/src/store/xyflow/hooks/
| Hook | Purpose |
|---|
useXYFlowNodeRenderData | Single subscription for all node render data |
useXYFlowNodeBodyPorts | Body port IDs for node body rendering |
useXYFlowNodeErrorPorts | Error port IDs for error section |
useXYFlowNodeFlowPorts | Flow port IDs (input/output) |
useXYFlowNodeHeaderData | Header data (title, category, etc.) |
Store Data Hooks
Location: apps/chaingraph-frontend/src/store/*/hooks/
| Hook | Purpose |
|---|
useXYFlowNodes | XYFlow-compatible nodes from Effector stores |
useXYFlowEdges | XYFlow-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}
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}
panOnScroll={true}
panOnDrag={panOnDrag}
selectionOnDrag={selectionOnDrag}
selectionMode={selectionMode}
zoomOnDoubleClick={true}
connectOnClick={true}
deleteKeyCode={['Delete', 'Backspace']}
fitView={true}
preventScrolling
minZoom={0.05}
maxZoom={2}
nodeDragThreshold={1}
nodesDraggable={!isGrabMode}
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
| File | Purpose |
|---|
components/flow/Flow.tsx | Main XYFlow container |
components/flow/nodes/ChaingraphNode/ChaingraphNodeOptimized.tsx | Optimized node wrapper |
components/flow/nodes/ChaingraphNode/ChaingraphNode.tsx | Main node component |
components/flow/nodes/AnchorNode/AnchorNode.tsx | Anchor node component |
components/flow/edges/FlowEdge.tsx | Custom edge with anchors |
components/flow/edges/index.ts | Edge type registration |
store/xyflow/types.ts | XYFlowNodeRenderData interface |
store/xyflow/stores/node-render-data.ts | $xyflowNodeRenderMap store |
store/xyflow/hooks/useXYFlowNodeRenderData.ts | Render data hook |
store/nodes/hooks/useXYFlowNodes.ts | Node data transformation |
store/edges/hooks/useXYFlowEdges.ts | Edge data transformation |
store/edges/anchor-nodes.ts | Anchor node store and events |
components/flow/hooks/ | 18 interaction hooks |
Common Patterns
Adding a Custom Node Type
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>
)
}
const nodeTypes = useMemo(() => ({
chaingraphNode: ChaingraphNodeOptimized,
groupNode: memo(GroupNode),
anchorNode: memo(AnchorNode),
myCustomNode: memo(MyCustomNode),
}), [])
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