Agent skill
xyflow-patterns
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/development/xyflow-patterns
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:
- 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, // 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/
| 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}
// 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
| 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
// 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 structureeffector-patterns- Store patterns usedsubscription-sync- Real-time node/edge updatesoptimistic-updates- Position interpolationchaingraph-concepts- Node/edge domain concepts
Didn't find tool you were looking for?