Skill v1.0.1
currentAutomated scan100/1003 files
version: "1.0.1" name: dagre-react-flow description: Automatic graph layout using dagre with React Flow (@xyflow/react). Use when implementing auto-layout, hierarchical layouts, tree structures, or arranging nodes programmatically. Triggers on dagre, auto-layout, automatic layout, getLayoutedElements, rankdir, hierarchical graph.
Dagre with React Flow
Dagre is a JavaScript library for laying out directed graphs. It computes optimal node positions for hierarchical/tree layouts. React Flow handles rendering; dagre handles positioning.
Quick Start
pnpm add @dagrejs/dagre
import dagre from '@dagrejs/dagre';import { Node, Edge } from '@xyflow/react';const getLayoutedElements = (nodes: Node[],edges: Edge[],direction: 'TB' | 'LR' = 'TB') => {const g = new dagre.graphlib.Graph();g.setGraph({ rankdir: direction });g.setDefaultEdgeLabel(() => ({}));nodes.forEach((node) => {g.setNode(node.id, { width: 172, height: 36 });});edges.forEach((edge) => {g.setEdge(edge.source, edge.target);});dagre.layout(g);const layoutedNodes = nodes.map((node) => {const pos = g.node(node.id);return {...node,position: { x: pos.x - 86, y: pos.y - 18 }, // Center to top-left};});return { nodes: layoutedNodes, edges };};
Core Concepts
Coordinate System Difference
Critical: Dagre returns center coordinates; React Flow uses top-left.
// Dagre output: center of nodeconst dagrePos = g.node(nodeId); // { x: 100, y: 50 } = center// React Flow expects: top-left cornerconst rfPosition = {x: dagrePos.x - nodeWidth / 2,y: dagrePos.y - nodeHeight / 2,};
Node Dimensions
Dagre requires explicit dimensions. Three approaches:
1. Fixed dimensions (simplest):
g.setNode(node.id, { width: 172, height: 36 });
2. Per-node dimensions from data:
g.setNode(node.id, {width: node.data.width ?? 172,height: node.data.height ?? 36,});
3. Measured dimensions (most accurate):
// After React Flow measures nodesg.setNode(node.id, {width: node.measured?.width ?? 172,height: node.measured?.height ?? 36,});
Layout Directions
| Value | Direction | Use Case | |
|---|---|---|---|
TB | Top to Bottom | Org charts, decision trees | |
BT | Bottom to Top | Dependency graphs (deps at bottom) | |
LR | Left to Right | Timelines, horizontal flows | |
RL | Right to Left | RTL layouts |
g.setGraph({ rankdir: 'LR' }); // Horizontal layout
Hard gates
Run these in order before treating layout as correct (each step has an objective pass condition):
- Dimensions match conversion — For every node id, the
widthandheightgiven tog.setNodefor that id are the same numbers used to computeposition.x/position.yfromg.node(id)(half-width / half-height must match the dagre node box). - Center → top-left —
positionis{ x: centerX - width/2, y: centerY - height/2 }, not rawg.node(id).x/.yalone. - React Flow state update — After programmatic layout,
setNodes/setEdgesreceive a new array instance (e.g.[...layouted]orlayouted.map(...)), not the previous reference unchanged. - Optional sanity — If you use
fitViewafter layout, it runs after nodes are committed (e.g. nextrequestAnimationFrameorsetTimeout(0)), not in the same synchronous tick assetNodeswith stale measurements.
Complete Implementation
Basic Layout Function
import dagre from '@dagrejs/dagre';import type { Node, Edge } from '@xyflow/react';interface LayoutOptions {direction?: 'TB' | 'BT' | 'LR' | 'RL';nodeWidth?: number;nodeHeight?: number;nodesep?: number; // Horizontal spacingranksep?: number; // Vertical spacing (between ranks)}export function getLayoutedElements(nodes: Node[],edges: Edge[],options: LayoutOptions = {}): { nodes: Node[]; edges: Edge[] } {const {direction = 'TB',nodeWidth = 172,nodeHeight = 36,nodesep = 50,ranksep = 50,} = options;const g = new dagre.graphlib.Graph();g.setGraph({ rankdir: direction, nodesep, ranksep });g.setDefaultEdgeLabel(() => ({}));nodes.forEach((node) => {const width = node.measured?.width ?? nodeWidth;const height = node.measured?.height ?? nodeHeight;g.setNode(node.id, { width, height });});edges.forEach((edge) => {g.setEdge(edge.source, edge.target);});dagre.layout(g);const layoutedNodes = nodes.map((node) => {const pos = g.node(node.id);const width = node.measured?.width ?? nodeWidth;const height = node.measured?.height ?? nodeHeight;return {...node,position: {x: pos.x - width / 2,y: pos.y - height / 2,},};});return { nodes: layoutedNodes, edges };}
React Flow Integration
import { useCallback } from 'react';import {ReactFlow,useNodesState,useEdgesState,useReactFlow,ReactFlowProvider,} from '@xyflow/react';import { getLayoutedElements } from './layout';const initialNodes = [{ id: '1', data: { label: 'Start' }, position: { x: 0, y: 0 } },{ id: '2', data: { label: 'Process' }, position: { x: 0, y: 0 } },{ id: '3', data: { label: 'End' }, position: { x: 0, y: 0 } },];const initialEdges = [{ id: 'e1-2', source: '1', target: '2' },{ id: 'e2-3', source: '2', target: '3' },];// Apply initial layoutconst { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(initialNodes,initialEdges,{ direction: 'TB' });function Flow() {const [nodes, setNodes, onNodesChange] = useNodesState(layoutedNodes);const [edges, setEdges, onEdgesChange] = useEdgesState(layoutedEdges);const { fitView } = useReactFlow();const onLayout = useCallback((direction: 'TB' | 'LR') => {const { nodes: newNodes, edges: newEdges } = getLayoutedElements(nodes,edges,{ direction });setNodes([...newNodes]);setEdges([...newEdges]);// Fit view after layout with animationwindow.requestAnimationFrame(() => {fitView({ duration: 300 });});}, [nodes, edges, setNodes, setEdges, fitView]);return (<div style={{ width: '100%', height: '100vh' }}><div style={{ position: 'absolute', zIndex: 10, padding: 10 }}><button onClick={() => onLayout('TB')}>Vertical</button><button onClick={() => onLayout('LR')}>Horizontal</button></div><ReactFlownodes={nodes}edges={edges}onNodesChange={onNodesChange}onEdgesChange={onEdgesChange}fitView/></div>);}export default function App() {return (<ReactFlowProvider><Flow /></ReactFlowProvider>);}
useAutoLayout Hook
Reusable hook for automatic layout:
import { useCallback, useEffect, useRef } from 'react';import {useReactFlow,useNodesInitialized,type Node,type Edge,} from '@xyflow/react';import dagre from '@dagrejs/dagre';interface UseAutoLayoutOptions {direction?: 'TB' | 'BT' | 'LR' | 'RL';nodesep?: number;ranksep?: number;}export function useAutoLayout(options: UseAutoLayoutOptions = {}) {const { direction = 'TB', nodesep = 50, ranksep = 50 } = options;const { getNodes, getEdges, setNodes, fitView } = useReactFlow();const nodesInitialized = useNodesInitialized();const layoutApplied = useRef(false);const runLayout = useCallback(() => {const nodes = getNodes();const edges = getEdges();const g = new dagre.graphlib.Graph();g.setGraph({ rankdir: direction, nodesep, ranksep });g.setDefaultEdgeLabel(() => ({}));nodes.forEach((node) => {g.setNode(node.id, {width: node.measured?.width ?? 172,height: node.measured?.height ?? 36,});});edges.forEach((edge) => {g.setEdge(edge.source, edge.target);});dagre.layout(g);const layouted = nodes.map((node) => {const pos = g.node(node.id);const width = node.measured?.width ?? 172;const height = node.measured?.height ?? 36;return {...node,position: { x: pos.x - width / 2, y: pos.y - height / 2 },};});setNodes(layouted);window.requestAnimationFrame(() => fitView({ duration: 200 }));}, [direction, nodesep, ranksep, getNodes, getEdges, setNodes, fitView]);// Auto-layout on initializationuseEffect(() => {if (nodesInitialized && !layoutApplied.current) {runLayout();layoutApplied.current = true;}}, [nodesInitialized, runLayout]);return { runLayout };}
Usage:
function Flow() {const { runLayout } = useAutoLayout({ direction: 'LR', ranksep: 100 });return (<><button onClick={runLayout}>Re-layout</button><ReactFlow ... /></>);}
Edge Options
Control edge routing with weight and minlen:
edges.forEach((edge) => {g.setEdge(edge.source, edge.target, {weight: edge.data?.priority ?? 1, // Higher = more direct pathminlen: edge.data?.minRanks ?? 1, // Minimum ranks between nodes});});
weight: Higher weight edges are prioritized for shorter, more direct paths.
minlen: Forces minimum rank separation between connected nodes.
// Force 2 ranks between nodesg.setEdge('a', 'b', { minlen: 2 });
Common Patterns
Handle Position Based on Direction
Adjust handles for horizontal vs vertical layouts:
function CustomNode({ data }: NodeProps) {const isHorizontal = data.direction === 'LR' || data.direction === 'RL';return (<div><Handletype="target"position={isHorizontal ? Position.Left : Position.Top}/><div>{data.label}</div><Handletype="source"position={isHorizontal ? Position.Right : Position.Bottom}/></div>);}
Animated Layout Transitions
Smooth position changes using CSS transitions:
.react-flow__node {transition: transform 300ms ease-out;}
For programmatic animation, see reference.md.
Layout with Node Groups
Exclude group nodes from dagre layout:
const layoutWithGroups = (nodes: Node[], edges: Edge[]) => {// Separate regular nodes from groupsconst regularNodes = nodes.filter((n) => n.type !== 'group');const groupNodes = nodes.filter((n) => n.type === 'group');// Layout only regular nodesconst { nodes: layouted } = getLayoutedElements(regularNodes, edges);// Combine backreturn { nodes: [...groupNodes, ...layouted], edges };};
Troubleshooting
Nodes Overlapping
Increase spacing:
g.setGraph({rankdir: 'TB',nodesep: 100, // Increase horizontal spacingranksep: 100, // Increase vertical spacing});
Layout Not Updating
Ensure new array references:
// Wrong - same referencesetNodes(layoutedNodes);// Correct - new referencesetNodes([...layoutedNodes]);
Nodes at Wrong Position
Check coordinate conversion:
// Dagre returns center, React Flow needs top-leftposition: {x: pos.x - width / 2, // Not just pos.xy: pos.y - height / 2, // Not just pos.y}
Performance with Large Graphs
- Layout in a Web Worker
- Debounce layout calls
- Use
useMemofor layout function - Only re-layout changed portions
Configuration Reference
See reference.md for complete dagre configuration options.