dagre-react-flow

Dagre with React Flow

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "dagre-react-flow" with this command: npx skills add existential-birds/beagle/existential-birds-beagle-dagre-react-flow

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 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 });

  1. Per-node dimensions from data:

g.setNode(node.id, { width: node.data.width ?? 172, height: node.data.height ?? 36, });

  1. 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 layout

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 useMemo for layout function

  • Only re-layout changed portions

Configuration Reference

See reference.md for complete dagre configuration options.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

tailwind-v4

No summary provided by upstream source.

Repository SourceNeeds Review
General

react-flow

No summary provided by upstream source.

Repository SourceNeeds Review
General

react-router-v7

No summary provided by upstream source.

Repository SourceNeeds Review
General

vitest-testing

No summary provided by upstream source.

Repository SourceNeeds Review