| 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