React Flow Architecture
by @anderskev
Architectural guidance for building node-based UIs with React Flow. Use when designing flow-based applications, making decisions about state management, inte...
clawhub install react-flow-architectureπ About This Skill
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
Consider Alternatives
Decision workflow (gates)
Run this sequence before locking the stack or sprinting implementation. Skip only for throwaway prototypes.
1. 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, β¦).
2. 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. onlyRenderVisibleElements when that row implies it).
3. 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.β
4. 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 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((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 ;
}
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:
extent: 'parent' to constrain draggingexpandParent: true to auto-expand parentViewport 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
// 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 |
Edge Rendering
| Default edges | Custom edges | |---------------|--------------| | Fast rendering | More control | | Limited styling | Any SVG/HTML | | Simple use cases | Complex labels |