SKILL.md
React Flow Architecture
When to Use React Flow
Good Fit
- Visual programming interfaces
- Workflow builders and automation tools
- Diagram editors (flowcharts, org charts)
- Data pipeline visualization
- Mind mapping tools
- Node-based audio/video editors
- Decision tree builders
- State machine designers
Consider Alternatives
- Simple static diagrams (use SVG or canvas directly)
- Heavy real-time collaboration (may need custom sync layer)
- 3D visualizations (use Three.js, react-three-fiber)
- Graph analysis with 10k+ nodes (use WebGL-based solutions like Sigma.js)
Decision workflow (gates)
Run this sequence before locking the stack or sprinting implementation. Skip only for throwaway prototypes.
-
Name the interactions — List the top user actions (e.g. drag, connect, delete, group). Pass: Each action maps to a concrete React Flow callback you will implement (onNodesChange, onConnect, …).
-
Classify scale — Estimate peak nodes (visible canvas or document total). Pass: Your range matches a row in [Node Count Guidelines](#node-count-guidelines) and you accept the listed strategy (e.g. onlyRenderVisibleElements when that row implies it).
-
Place state — Choose local hooks, an external store, or Redux/other. Pass: One sentence states where persistence, undo, or cross-surface sync will live, or explicitly “not needed yet.”
-
Re-check alternatives — If the use case matches [Consider Alternatives](#consider-alternatives), Pass: One sentence explains why React Flow still fits or which listed alternative you chose instead.
Architecture Patterns
Package Structure (xyflow)
@xyflow/system (vanilla TypeScript)
├── Core algorithms (edge paths, bounds, viewport)
├── xypanzoom (d3-based pan/zoom)
├── xydrag, xyhandle, xyminimap, xyresizer
└── Shared types
@xyflow/react (depends on @xyflow/system)
├── React components and hooks
├── Zustand store for state management
└── Framework-specific integrations
@xyflow/svelte (depends on @xyflow/system)
└── Svelte components and stores
Implication: Core logic is framework-agnostic. When contributing or debugging, check if issue is in @xyflow/system or framework-specific package.
State Management Approaches
#### 1. Local State (Simple Apps)
// useNodesState/useEdgesState for prototyping
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
Pros: Simple, minimal boilerplate
Cons: State isolated to component tree
#### 2. External Store (Production)
// Zustand store example
import { create } from 'zustand';
interface FlowStore {
nodes: Node[];
edges: Edge[];
setNodes: (nodes: Node[]) => void;
onNodesChange: OnNodesChange;
}
const useFlowStore = create<FlowStore>((set, get) => ({
nodes: initialNodes,
edges: initialEdges,
setNodes: (nodes) => set({ nodes }),
onNodesChange: (changes) => {
set({ nodes: applyNodeChanges(changes, get().nodes) });
},
}));
// In component
function Flow() {
const { nodes, edges, onNodesChange } = useFlowStore();
return <ReactFlow nodes={nodes} onNodesChange={onNodesChange} />;
}
Pros: State accessible anywhere, easier persistence/sync
Cons: More setup, need careful selector optimization
#### 3. Redux/Other State Libraries
// Connect via selectors
const nodes = useSelector(selectNodes);
const dispatch = useDispatch();
const onNodesChange = useCallback((changes: NodeChange[]) => {
dispatch(nodesChanged(changes));
}, [dispatch]);
Data Flow Architecture
User Input → Change Event → Reducer/Handler → State Update → Re-render
↓
[Drag node] → onNodesChange → applyNodeChanges → setNodes → ReactFlow
↓
[Connect] → onConnect → addEdge → setEdges → ReactFlow
↓
[Delete] → onNodesDelete → deleteElements → setNodes/setEdges → ReactFlow
Sub-Flow Pattern (Nested Nodes)
// Parent node containing child nodes
const nodes = [
{
id: 'group-1',
type: 'group',
position: { x: 0, y: 0 },
style: { width: 300, height: 200 },
},
{
id: 'child-1',
parentId: 'group-1', // Key: parent reference
extent: 'parent', // Key: constrain to parent
position: { x: 10, y: 30 }, // Relative to parent
data: { label: 'Child' },
},
];
Considerations:
- Use
extent: 'parent'to constrain dragging
- Use
expandParent: trueto auto-expand parent
- Parent z-index affects child rendering order
Viewport Persistence
// Save viewport state
const { toObject, setViewport } = useReactFlow();
const handleSave = () => {
const flow = toObject();
// flow.nodes, flow.edges, flow.viewport
localStorage.setItem('flow', JSON.stringify(flow));
};
const handleRestore = () => {
const flow = JSON.parse(localStorage.getItem('flow'));
setNodes(flow.nodes);
setEdges(flow.edges);
setViewport(flow.viewport);
};
Integration Patterns
With Backend/API
// Load from API
useEffect(() => {
fetch('/api/flow')
.then(r => r.json())
.then(({ nodes, edges }) => {
setNodes(nodes);
setEdges(edges);
});
}, []);
// Debounced auto-save
const debouncedSave = useMemo(
() => debounce((nodes, edges) => {
fetch('/api/flow', {
method: 'POST',
body: JSON.stringify({ nodes, edges }),
});
}, 1000),
[]
);
useEffect(() => {
debouncedSave(nodes, edges);
}, [nodes, edges]);
With Layout Algorithms
import dagre from 'dagre';
function getLayoutedElements(nodes: Node[], edges: Edge[]) {
const g = new dagre.graphlib.Graph();
g.setGraph({ rankdir: 'TB' });
g.setDefaultEdgeLabel(() => ({}));
nodes.forEach((node) => {
g.setNode(node.id, { width: 150, height: 50 });
});
edges.forEach((edge) => {
g.setEdge(edge.source, edge.target);
});
dagre.layout(g);
return {
nodes: nodes.map((node) => {
const pos = g.node(node.id);
return { ...node, position: { x: pos.x, y: pos.y } };
}),
edges,
};
}
Performance Scaling
Node Count Guidelines
Nodes
Strategy
< 100
Default settings
100-500
Enable onlyRenderVisibleElements
500-1000
Simplify custom nodes, reduce DOM elements
1000
Consider virtualization, WebGL alternatives
Optimization Techniques
<ReactFlow
// Only render nodes/edges in viewport
onlyRenderVisibleElements={true}
// Reduce node border radius (improves intersect calculations)
nodeExtent={[[-1000, -1000], [1000, 1000]]}
// Disable features not needed
elementsSelectable={false}
panOnDrag={false}
zoomOnScroll={false}
/>
Trade-offs
Controlled vs Uncontrolled
Controlled
Uncontrolled
More boilerplate
Less code
Full state control
Internal state
Easy persistence
Need toObject()
Better for complex apps
Good for prototypes
Connection Modes
Strict (default)
Loose
Source → Target only
Any handle → any handle
Predictable behavior
More flexible
Use for data flows
Use for diagrams
<ReactFlow connectionMode={ConnectionMode.Loose} />
Edge Rendering
Default edges
Custom edges
Fast rendering
More control
Limited styling
Any SVG/HTML
Simple use cases
Complex labels