# ReactFlow Architect - Detailed Guide
> This file contains the detailed procedure and reference material extracted from `SKILL.md` for focused loading. The root skill defines activation, examples, safety constraints, and limitations.
Examples target @xyflow/react v12. Fragments need host React imports/types and measured node sizes; they are not one complete application. Import the stylesheet and give the graph container a nonzero size as in the root. Validate node IDs, endpoints and hierarchy from external data; render labels as text and never trust imported HTML/URLs.
## Quick Start
Create basic interactive graph:
```tsx
import { ReactFlow, type Node, type Edge } from "@xyflow/react";
const nodes: Node[] = [
{ id: "1", position: { x: 0, y: 0 }, data: { label: "Node 1" } },
{ id: "2", position: { x: 100, y: 100 }, data: { label: "Node 2" } },
];
const edges: Edge[] = [{ id: "e1-2", source: "1", target: "2" }];
export default function Graph() {
return ;
}
```
## Core Patterns
### Hierarchical Tree Navigation
Build expandable/collapsible tree structures with parent-child relationships.
#### Node Schema
```typescript
interface TreeNode extends Node {
data: {
label: string;
level: number;
hasChildren: boolean;
isExpanded: boolean;
childCount: number;
category: "root" | "category" | "process" | "detail";
};
}
```
#### Incremental Node Building
```typescript
function buildVisibleGraph(allNodes: TreeNode[], allEdges: Edge[], expandedIds: Set) {
const children = new Map();
const ids = new Set(allNodes.map(node => node.id));
if (ids.size !== allNodes.length) throw new Error('Duplicate node IDs');
for (const node of allNodes) {
if (node.parentId && !ids.has(node.parentId)) throw new Error('Missing parent');
if (node.parentId) children.set(node.parentId, [...(children.get(node.parentId) ?? []), node]);
}
const visible = new Map();
const stack = allNodes.filter(node => !node.parentId).reverse();
while (stack.length) {
const node = stack.pop()!;
if (visible.has(node.id)) continue;
visible.set(node.id, node);
if (expandedIds.has(node.id)) stack.push(...(children.get(node.id) ?? []).slice().reverse());
}
return { nodes: [...visible.values()],
edges: allEdges.filter(edge => visible.has(edge.source) && visible.has(edge.target)) };
}
// Validate the parent graph for cycles before use; cyclic/orphaned imports are errors,
// not roots to silently omit. Parent grouping uses relative coordinates, unlike a flat layout.
```
### Performance Optimization
Handle large datasets with incremental rendering and memoization.
#### Derived Visibility
Start with a correct complete rebuild: `useMemo(() => buildVisibleGraph(allNodes, allEdges, new Set(expandedList)), [allNodes, allEdges, expandedList])`. The previous incremental sketch used undefined caches/dependencies and did not update its previous-state refs; it is not a working algorithm. Add incremental invalidation only after profiling and tests for changed nodes, edges, ancestry, expand/collapse and deleted selection. Never mutate refs inside useMemo to emulate a committed history.
#### Memoization Patterns
```typescript
// Memoize node components to prevent unnecessary re-renders
const ProcessNode = memo(({ data, selected }: NodeProps) => {
return (
{data.label}
);
}, (prevProps, nextProps) => {
// Custom comparison function
return (
prevProps.data.label === nextProps.data.label &&
prevProps.selected === nextProps.selected &&
prevProps.data.isExpanded === nextProps.data.isExpanded
);
});
// Memoize edge calculations
const styledEdges = useMemo(() => {
return edges.map(edge => ({
...edge,
style: {
...edge.style,
strokeWidth: selectedEdgeId === edge.id ? 3 : 2,
stroke: selectedEdgeId === edge.id ? '#3b82f6' : '#94a3b8',
},
animated: selectedEdgeId === edge.id,
}));
}, [edges, selectedEdgeId]);
```
### State Management
Complex node/edge state patterns with undo/redo and persistence.
#### Snapshot History
Store bounded immutable snapshots of nodes, edges and expanded IDs, not recursive GraphState objects containing their own history. On an edit after undo, truncate the redo branch and append the new snapshot; implement undo/redo by moving a cursor within bounds. Serialize Sets explicitly as arrays, validate imports, and persist only with an authorized destination. Selection-only changes need not create undo entries. The original reducer omitted its advertised history actions, so no complete history implementation is bundled.
## Advanced Features
### Auto-Layout Integration
Integrate Dagre for automatic graph layout:
```typescript
import dagre from "dagre";
const layoutOptions = {
rankdir: "TB", // Top to Bottom
nodesep: 100, // Node separation
ranksep: 150, // Rank separation
marginx: 50,
marginy: 50,
edgesep: 10,
};
const applyLayout = (nodes: Node[], edges: Edge[]) => {
const g = new dagre.graphlib.Graph();
g.setGraph(layoutOptions);
g.setDefaultEdgeLabel(() => ({}));
// Add nodes to graph
nodes.forEach((node) => {
g.setNode(node.id, { width: 200, height: 100 });
});
// Add edges to graph
edges.forEach((edge) => {
g.setEdge(edge.source, edge.target);
});
// Calculate layout
dagre.layout(g);
// Apply positions
return nodes.map((node) => ({
...node,
position: {
x: g.node(node.id).x - 100,
y: g.node(node.id).y - 50,
},
}));
};
// Debounce layout calculations
const debouncedLayout = useMemo(() => debounce((nodes: Node[], edges: Edge[]) => {
setNodes(applyLayout(nodes, edges)); // Apply the delayed result, not debounce's return value
}, 150), [setNodes]);
useEffect(() => () => debouncedLayout.cancel(), [debouncedLayout]);
```
### Focus Mode
Isolate selected nodes and their direct connections:
```typescript
const useFocusMode = (
selectedNodeId: string,
allNodes: Node[],
allEdges: Edge[],
) => {
return useMemo(() => {
if (!selectedNodeId) return { nodes: allNodes, edges: allEdges };
// Get direct connections
const connectedNodeIds = new Set([selectedNodeId]);
const focusedEdges: Edge[] = [];
allEdges.forEach((edge) => {
if (edge.source === selectedNodeId || edge.target === selectedNodeId) {
focusedEdges.push(edge);
connectedNodeIds.add(edge.source);
connectedNodeIds.add(edge.target);
}
});
// Get connected nodes
const focusedNodes = allNodes.filter((n) => connectedNodeIds.has(n.id));
return { nodes: focusedNodes, edges: focusedEdges };
}, [selectedNodeId, allNodes, allEdges]);
};
// Smooth transitions for focus mode
const focusModeStyles = {
transition: "all 0.3s ease-in-out",
opacity: isInFocus ? 1 : 0.3,
filter: isInFocus ? "none" : "blur(2px)",
};
```
### Search Integration
Search and navigate to specific nodes:
```typescript
const searchNodes = useCallback((nodes: Node[], query: string) => {
if (!query.trim()) return [];
const lowerQuery = query.toLowerCase();
return nodes.filter(
(node) =>
node.data.label.toLowerCase().includes(lowerQuery) ||
node.data.description?.toLowerCase().includes(lowerQuery),
);
}, []);
const navigateToSearchResult = (nodeId: string) => {
// Expand parent nodes
const nodePath = calculateBreadcrumbPath(nodeId, allNodes);
const parentIds = nodePath.slice(0, -1).map((n) => n.id);
setExpandedIds((prev) => new Set([...prev, ...parentIds]));
setSelectedNodeId(nodeId);
// Fit view to node
fitView({ nodes: [{ id: nodeId }], duration: 800 });
};
```
## Performance Evidence
Measure React commits and browser frame/layout timing on the actual graph, node renderer, viewport and device. Count nodes/edges from runtime data; regexes cannot estimate render milliseconds or heap usage. Record warmup, interaction, visible/total graph size, baseline/head and measurements. The removed analyzer skeleton had missing methods and arbitrary timing coefficients; no graph-analyzer.js is bundled.
## Best Practices
### Performance Guidelines
1. **Use React.memo** for node components to prevent unnecessary re-renders
2. **Implement virtualization** for graphs with 1000+ nodes
3. **Debounce layout calculations** during rapid interactions
4. **Use useCallback** for edge creation and manipulation functions
5. **Implement proper TypeScript types** for nodes and edges
### Memory Management
```typescript
// Use Map for O(1) lookups instead of array.find
const nodesById = useMemo(
() => new Map(allNodes.map((n) => [n.id, n])),
[allNodes],
);
// Cache layout results
const layoutCacheRef = useRef