
Dagre React Flow
- 208 installs
- 74 repo stars
- Updated July 21, 2026
- existential-birds/beagle
Wire Dagre auto-layout into React Flow for workflow editors, dependency graphs, and agent canvas UIs with correct node spacing and edge routing.
About
Guides implementation of Dagre-powered automatic graph layout inside React Flow, covering node sizing, directed layout configs, fit-view behavior, and performant updates for workflow and agent diagram UIs.
- Dagre layout
- React Flow hooks
- Auto-positioning
- Edge routing
- Canvas UX
Dagre React Flow by the numbers
- 208 all-time installs (skills.sh)
- Ranked #847 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/existential-birds/beagle --skill dagre-react-flowAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 208 |
|---|---|
| repo stars | ★ 74 |
| Last updated | July 21, 2026 |
| Repository | existential-birds/beagle ↗ |
What it does
Wire Dagre auto-layout into React Flow for workflow editors, dependency graphs, and agent canvas UIs with correct node spacing and edge routing.
Files
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/dagreimport 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 node
const dagrePos = g.node(nodeId); // { x: 100, y: 50 } = center
// React Flow expects: top-left corner
const 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 nodes
g.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 layoutHard gates
Run these in order before treating layout as correct (each step has an objective pass condition):
1. Dimensions match conversion — For every node id, the width and height given to g.setNode for that id are the same numbers used to compute position.x / position.y from g.node(id) (half-width / half-height must match the dagre node box). 2. Center → top-left — position is { x: centerX - width/2, y: centerY - height/2 }, not raw g.node(id).x / .y alone. 3. React Flow state update — After programmatic layout, setNodes / setEdges receive a new array instance (e.g. [...layouted] or layouted.map(...)), not the previous reference unchanged. 4. Optional sanity — If you use fitView after layout, it runs after nodes are committed (e.g. next requestAnimationFrame or setTimeout(0)), not in the same synchronous tick as setNodes with 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 spacing
ranksep?: 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 layout
const { 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 animation
window.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>
<ReactFlow
nodes={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 initialization
useEffect(() => {
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 path
minlen: 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 nodes
g.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>
<Handle
type="target"
position={isHorizontal ? Position.Left : Position.Top}
/>
<div>{data.label}</div>
<Handle
type="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 groups
const regularNodes = nodes.filter((n) => n.type !== 'group');
const groupNodes = nodes.filter((n) => n.type === 'group');
// Layout only regular nodes
const { nodes: layouted } = getLayoutedElements(regularNodes, edges);
// Combine back
return { nodes: [...groupNodes, ...layouted], edges };
};Troubleshooting
Nodes Overlapping
Increase spacing:
g.setGraph({
rankdir: 'TB',
nodesep: 100, // Increase horizontal spacing
ranksep: 100, // Increase vertical spacing
});Layout Not Updating
Ensure new array references:
// Wrong - same reference
setNodes(layoutedNodes);
// Correct - new reference
setNodes([...layoutedNodes]);Nodes at Wrong Position
Check coordinate conversion:
// Dagre returns center, React Flow needs top-left
position: {
x: pos.x - width / 2, // Not just pos.x
y: 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.
Dagre Configuration Reference
Complete configuration options for dagre layout algorithm.
Graph-Level Options
Set via g.setGraph(options):
| Option | Default | Description |
|---|---|---|
rankdir | 'TB' | Layout direction: 'TB' (top-bottom), 'BT' (bottom-top), 'LR' (left-right), 'RL' (right-left) |
align | undefined | Node alignment within rank: 'UL', 'UR', 'DL', 'DR'. U=up, D=down, L=left, R=right |
nodesep | 50 | Horizontal spacing between nodes in same rank (pixels) |
edgesep | 10 | Horizontal spacing between edges (pixels) |
ranksep | 50 | Vertical spacing between ranks (pixels) |
marginx | 0 | Horizontal margin around graph (pixels) |
marginy | 0 | Vertical margin around graph (pixels) |
acyclicer | undefined | Set to 'greedy' for greedy cycle removal heuristic |
ranker | 'network-simplex' | Rank assignment algorithm: 'network-simplex', 'tight-tree', 'longest-path' |
Example
g.setGraph({
rankdir: 'LR', // Horizontal layout
align: 'UL', // Align nodes to upper-left
nodesep: 80, // 80px horizontal spacing
ranksep: 100, // 100px between ranks
marginx: 20, // 20px horizontal margin
marginy: 20, // 20px vertical margin
ranker: 'tight-tree', // Faster ranking algorithm
});Ranker Algorithms
| Algorithm | Speed | Quality | Use Case |
|---|---|---|---|
network-simplex | Slower | Best | Default, optimal for most graphs |
tight-tree | Fast | Good | Large graphs where speed matters |
longest-path | Fastest | Acceptable | Very large graphs, quick preview |
Node-Level Options
Set via g.setNode(nodeId, options):
| Option | Default | Description |
|---|---|---|
width | 0 | Node width in pixels (required for layout) |
height | 0 | Node height in pixels (required for layout) |
Output Properties
After dagre.layout(g), each node gains:
| Property | Description |
|---|---|
x | Center x-coordinate |
y | Center y-coordinate |
Example
// Setting node dimensions
g.setNode('node-1', { width: 200, height: 50 });
// After layout, reading position
dagre.layout(g);
const { x, y } = g.node('node-1'); // Center coordinatesEdge-Level Options
Set via g.setEdge(source, target, options):
| Option | Default | Description |
|---|---|---|
minlen | 1 | Minimum number of ranks between source and target |
weight | 1 | Edge weight for prioritization (higher = shorter path) |
width | 0 | Edge label width in pixels |
height | 0 | Edge label height in pixels |
labelpos | 'r' | Label position: 'l' (left), 'c' (center), 'r' (right) |
labeloffset | 10 | Pixels to offset label from edge |
Output Properties
After dagre.layout(g), each edge gains:
| Property | Description |
|---|---|
points | Array of {x, y} control points for edge path |
x | Label center x-coordinate (if label dimensions set) |
y | Label center y-coordinate (if label dimensions set) |
Example
// High priority edge (shorter path)
g.setEdge('a', 'b', { weight: 2 });
// Force separation of 3 ranks
g.setEdge('a', 'c', { minlen: 3 });
// Edge with label
g.setEdge('a', 'd', {
width: 50,
height: 20,
labelpos: 'c',
});
// After layout
dagre.layout(g);
const edge = g.edge('a', 'b');
console.log(edge.points); // [{x: 0, y: 0}, {x: 50, y: 50}, ...]Graph Methods
Reading Graph State
// Get all node IDs
const nodeIds = g.nodes(); // ['a', 'b', 'c']
// Get all edges
const edges = g.edges(); // [{v: 'a', w: 'b'}, ...]
// Check if node exists
g.hasNode('a'); // true/false
// Check if edge exists
g.hasEdge('a', 'b'); // true/false
// Get node data
g.node('a'); // { width: 100, height: 50, x: 200, y: 100 }
// Get edge data
g.edge('a', 'b'); // { points: [...], weight: 1 }Modifying Graph
// Remove node (also removes connected edges)
g.removeNode('a');
// Remove edge
g.removeEdge('a', 'b');
// Get predecessors (nodes with edges TO this node)
g.predecessors('b'); // ['a']
// Get successors (nodes with edges FROM this node)
g.successors('a'); // ['b', 'c']
// Get all connected nodes (in + out)
g.neighbors('a'); // ['b', 'c', 'd']TypeScript Types
import dagre from '@dagrejs/dagre';
interface GraphOptions {
rankdir?: 'TB' | 'BT' | 'LR' | 'RL';
align?: 'UL' | 'UR' | 'DL' | 'DR';
nodesep?: number;
edgesep?: number;
ranksep?: number;
marginx?: number;
marginy?: number;
acyclicer?: 'greedy';
ranker?: 'network-simplex' | 'tight-tree' | 'longest-path';
}
interface NodeOptions {
width: number;
height: number;
}
interface NodeOutput extends NodeOptions {
x: number;
y: number;
}
interface EdgeOptions {
minlen?: number;
weight?: number;
width?: number;
height?: number;
labelpos?: 'l' | 'c' | 'r';
labeloffset?: number;
}
interface EdgeOutput extends EdgeOptions {
points: Array<{ x: number; y: number }>;
x?: number; // If label dimensions set
y?: number; // If label dimensions set
}Performance Considerations
Graph Size Guidelines
| Nodes | Performance | Recommendation |
|---|---|---|
| < 100 | Fast | Use network-simplex |
| 100-500 | Moderate | Consider tight-tree |
| 500-1000 | Slow | Use longest-path, layout in worker |
| > 1000 | Very slow | Virtualize, paginate, or use WebGL renderer |
Optimization Tips
1. Reuse graph instance when only positions change 2. Layout in Web Worker for graphs > 200 nodes 3. Debounce layout calls during rapid changes 4. Cache layout results for static portions
Web Worker Example
// layout.worker.ts
import dagre from '@dagrejs/dagre';
self.onmessage = (e) => {
const { nodes, edges, options } = e.data;
const g = new dagre.graphlib.Graph();
g.setGraph(options);
g.setDefaultEdgeLabel(() => ({}));
nodes.forEach((n) => g.setNode(n.id, { width: n.width, height: n.height }));
edges.forEach((e) => g.setEdge(e.source, e.target));
dagre.layout(g);
const positions = nodes.map((n) => ({
id: n.id,
x: g.node(n.id).x,
y: g.node(n.id).y,
}));
self.postMessage({ positions });
};Comparison with Alternatives
| Library | Best For | Bundle Size | Async |
|---|---|---|---|
| dagre | Trees, hierarchies | ~30KB | No |
| elkjs | Complex constraints | ~150KB | Yes |
| d3-hierarchy | Pure trees only | ~10KB | No |
| d3-force | Organic layouts | ~15KB | Iterative |
Choose dagre when:
- Graph is hierarchical/tree-like
- Need simple, fast layouts
- Bundle size matters
- Don't need edge routing around nodes
Animated Layout Transitions
Programmatic animation for smooth position changes:
const animateLayout = (
currentNodes: Node[],
newNodes: Node[],
setNodes: (nodes: Node[]) => void,
duration = 300
) => {
const startPositions = new Map(
currentNodes.map((n) => [n.id, { ...n.position }])
);
const animate = (progress: number) => {
const interpolated = newNodes.map((node) => {
const start = startPositions.get(node.id);
if (!start) return node;
return {
...node,
position: {
x: start.x + (node.position.x - start.x) * progress,
y: start.y + (node.position.y - start.y) * progress,
},
};
});
setNodes(interpolated);
};
const startTime = Date.now();
const tick = () => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
// Ease-out curve
const eased = 1 - Math.pow(1 - progress, 3);
animate(eased);
if (progress < 1) requestAnimationFrame(tick);
};
tick();
};
// Usage
const onLayout = (direction: 'TB' | 'LR') => {
const { nodes: layouted } = getLayoutedElements(nodes, edges, { direction });
animateLayout(nodes, layouted, setNodes, 400);
};