Skill v1.0.1
currentLLM-judged scan95/100+2 new, ~1 modified
version: "1.0.1" name: react-flow-architecture description: Architectural guidance for building node-based UIs with React Flow. Use when designing flow-based applications, making decisions about state management, integration patterns, or evaluating whether React Flow fits a use case.
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 and you accept the listed strategy (e.g.
onlyRenderVisibleElementswhen 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, 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 prototypingconst [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 exampleimport { 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 componentfunction 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 selectorsconst 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 nodesconst nodes = [{id: 'group-1',type: 'group',position: { x: 0, y: 0 },style: { width: 300, height: 200 },},{id: 'child-1',parentId: 'group-1', // Key: parent referenceextent: 'parent', // Key: constrain to parentposition: { x: 10, y: 30 }, // Relative to parentdata: { 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 stateconst { toObject, setViewport } = useReactFlow();const handleSave = () => {const flow = toObject();// flow.nodes, flow.edges, flow.viewportlocalStorage.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 APIuseEffect(() => {fetch('/api/flow').then(r => r.json()).then(({ nodes, edges }) => {setNodes(nodes);setEdges(edges);});}, []);// Debounced auto-saveconst 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 viewportonlyRenderVisibleElements={true}// Reduce node border radius (improves intersect calculations)nodeExtent={[[-1000, -1000], [1000, 1000]]}// Disable features not neededelementsSelectable={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 |