@xyflow/react
@xyflow/react is React Flow, the library behind most node-based editors you see in React apps: workflow builders, AI agent canvases, ETL pipelines, visual programming tools. It gives you a pan-and-zoom canvas (d3-zoom and d3-drag under the hood, via the shared @xyflow/system package) where every node is a plain React component you write, every edge is an SVG path, and the whole graph is state you own and pass in as props. Dragging, multi-select, keyboard shortcuts, connection lines between handles, and plugin components like MiniMap, Controls and Background all work out of the box. This is version 12 of the library formerly published as reactflow (and react-flow-renderer before that); the xyflow monorepo also ships Svelte Flow. MIT licensed, funded by paid Pro example subscriptions.
The default choice for node-based UIs in React, and deservedly so: 10.2M weekly downloads, real company maintenance, excellent docs. Just know going in that layout is your problem, the fancy examples cost money, and v12 broke with everything the internet taught about v11.
Use it if
- You are building an editor where nodes are real interactive React components: a workflow builder, an AI agent graph, a data pipeline UI, an org chart with buttons and forms inside nodes
- You want dragging, box selection, zoom, snap-to-grid, connection drawing and keyboard accessibility handled for you, with TypeScript types for nodes, edges and every callback
- You need the graph to be controlled state so it can live in your own zustand or redux store and sync to a backend, which the applyNodeChanges and applyEdgeChanges helpers make straightforward
- You want a maintained dependency: a company behind it, releases roughly monthly (12.11.0 in June 2026, 12.11.2 in July 2026), and a repo pushed to today at 37.9k stars
- You need a static diagram, not an editor: 57.4 KB gzip plus a full interaction model is a lot of machinery for a picture. Mermaid or plain SVG renders a flowchart with none of it
- You expect automatic layout: React Flow ships zero layout algorithms. You wire up dagre or elkjs yourself, and the polished versions of exactly the features people want next (one-click auto layout, undo/redo, helper lines, copy-paste, collaborative editing, lasso selection) are paid Pro examples behind a subscription
- You object to the corner attribution: the canvas renders a React Flow credit link, and the sanctioned way to remove it is a Pro subscription. The license is MIT, but the project is explicit that attribution removal is what you pay for
- You are rendering thousands of complex nodes: it is DOM, not canvas or WebGL. Every drag tick flows through state, and past several hundred non-trivial nodes you are into serious memoization work, hidden-node tricks, and stripped-down CSS. Cytoscape or a WebGL renderer scales further for pure graph visualization
- You are following older tutorials: the v11 package reactflow and its APIs (project(), parentNode, node.width as a measured value) are all over Stack Overflow and blog posts, and none of them work unchanged against @xyflow/react v12
Setup reality
npm install @xyflow/react is easy; the required import '@xyflow/react/dist/style.css' is not optional, and forgetting it gives you a broken, unstyled mess (base.css is the minimal alternative). The number one first-hour bug is a blank screen because the parent container has no explicit width and height, since the canvas fills its parent. Peer deps are just react and react-dom 17 or newer; zustand 4 and classcat come bundled as regular dependencies, so no store conflicts with your own zustand 5. The gotcha that actually costs time is reference stability: nodeTypes, edgeTypes, defaultEdgeOptions and snapGrid must be defined at module scope or memoized, or React Flow warns (error 002) and remounts your custom nodes every render. Migrating from the reactflow package means renamed imports, project() becoming screenToFlowPosition, parentNode becoming parentId, and measured dimensions moving to node.measured.
Patterns
Render a minimal flowbasic-flow
import { ReactFlow } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
const nodes = [
{ id: '1', position: { x: 0, y: 0 }, data: { label: 'Input' } },
{ id: '2', position: { x: 0, y: 100 }, data: { label: 'Output' } },
];
const edges = [{ id: 'e1-2', source: '1', target: '2' }];
export default function App() {
return (
<div style={{ width: '100vw', height: '100vh' }}>
<ReactFlow nodes={nodes} edges={edges} fitView />
</div>
);
}The stylesheet import is mandatory and the parent element needs explicit dimensions, or you get a blank or unstyled canvas: the two most common first bugs. Passing nodes as a prop makes the flow controlled, so without onNodesChange nothing is draggable yet.
Make nodes draggable and connectableinteractive-flow
import { useCallback } from 'react';
import { ReactFlow, useNodesState, useEdgesState, addEdge } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
function Flow() {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback(
(connection) => setEdges((eds) => addEdge(connection, eds)),
[setEdges],
);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
/>
);
}useNodesState and useEdgesState are thin wrappers over useState plus applyNodeChanges. Skip onConnect and connections users draw between handles silently vanish, because in controlled mode you must commit every change yourself.
Keep the graph in your own storeexternal-state
import { useState, useCallback } from 'react';
import { applyNodeChanges, applyEdgeChanges } from '@xyflow/react';
const [nodes, setNodes] = useState(initialNodes);
const [edges, setEdges] = useState(initialEdges);
const onNodesChange = useCallback(
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
[],
);
const onEdgesChange = useCallback(
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
[],
);This is what useNodesState does internally, spelled out so the same reducers can live in zustand, redux or jotai. Drags, selections and deletions all arrive as change objects, which is the hook point for autosave or undo history.
Build a custom node with handlescustom-node
import { Handle, Position } from '@xyflow/react';
function ScoreNode({ data }) {
return (
<div className="score-node">
<Handle type="target" position={Position.Top} />
<strong>{data.label}</strong>
<Handle type="source" position={Position.Bottom} id="pass" />
<Handle type="source" position={Position.Bottom} id="fail" style={{ left: 60 }} />
</div>
);
}
const nodeTypes = { score: ScoreNode }; // module scope, on purpose
<ReactFlow nodes={nodes} edges={edges} nodeTypes={nodeTypes} />nodeTypes must be defined outside the component (or wrapped in useMemo), otherwise React Flow logs warning 002 and remounts every custom node on each render. Two handles of the same type need unique ids, and edges pick one with sourceHandle: 'pass'.
Add labels, arrows and animation to edgesedge-labels-markers
import { MarkerType } from '@xyflow/react';
const edges = [
{
id: 'e1-2',
source: '1',
target: '2',
type: 'smoothstep',
label: 'on success',
animated: true,
markerEnd: { type: MarkerType.ArrowClosed, width: 20, height: 20 },
},
];Built-in edge types are default (bezier), straight, step and smoothstep. Edges have no arrowheads unless you set markerEnd; older code using ArrowHeadType is pre-v11. For labels beyond a plain string, write a custom edge with EdgeLabelRenderer.
Control the viewport with useReactFlowviewport-control
import { ReactFlow, ReactFlowProvider, useReactFlow } from '@xyflow/react';
function FitButton() {
const { fitView, zoomTo } = useReactFlow();
return (
<button onClick={() => fitView({ padding: 0.2, duration: 300 })}>
Fit view
</button>
);
}
export default function App() {
return (
<ReactFlowProvider>
<ReactFlow nodes={nodes} edges={edges} />
<FitButton />
</ReactFlowProvider>
);
}useReactFlow only works inside a ReactFlowProvider (or as a child of ReactFlow itself); called outside, the store is empty and helpers do nothing. The instance also gives you addNodes, deleteElements, getNodes and setViewport for imperative work.
Add a node where the user clickedadd-node-at-position
import { useCallback } from 'react';
import { useReactFlow } from '@xyflow/react';
const { screenToFlowPosition, addNodes } = useReactFlow();
const onPaneClick = useCallback(
(event) => {
const position = screenToFlowPosition({
x: event.clientX,
y: event.clientY,
});
addNodes({ id: crypto.randomUUID(), position, data: { label: 'new' } });
},
[screenToFlowPosition, addNodes],
);
<ReactFlow onPaneClick={onPaneClick} /* ... */ />screenToFlowPosition converts screen pixels to canvas coordinates accounting for pan and zoom; it replaced project() in v12, which old drag-and-drop tutorials still use. The same call powers dropping nodes from a sidebar palette.
Auto-layout a flow with dagre (DIY)auto-layout-dagre
import Dagre from '@dagrejs/dagre';
function layout(nodes, edges, direction = 'TB') {
const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
g.setGraph({ rankdir: direction });
edges.forEach((e) => g.setEdge(e.source, e.target));
nodes.forEach((n) =>
g.setNode(n.id, {
width: n.measured?.width ?? 0,
height: n.measured?.height ?? 0,
}),
);
Dagre.layout(g);
return nodes.map((n) => {
const { x, y } = g.node(n.id);
return {
...n,
position: {
x: x - (n.measured?.width ?? 0) / 2,
y: y - (n.measured?.height ?? 0) / 2,
},
};
});
}React Flow includes no layout algorithms; this is the official docs recipe, with elkjs as the heavier option for ports and edge routing. node.measured only exists after the first render, so lay out in a button handler or effect, not during initial state. The slick one-click auto-layout demo on their site is a paid Pro example.
Add minimap, zoom controls and a dotted backgroundminimap-controls-background
import {
ReactFlow,
MiniMap,
Controls,
Background,
BackgroundVariant,
Panel,
} from '@xyflow/react';
<ReactFlow nodes={nodes} edges={edges}>
<Background variant={BackgroundVariant.Dots} gap={16} />
<MiniMap pannable zoomable />
<Controls showInteractive={false} />
<Panel position="top-left">Your own toolbar here</Panel>
</ReactFlow>All four ship in the core package as children of ReactFlow. Panel is the escape hatch for arbitrary overlay UI positioned on the canvas. MiniMap used to re-render on every store update; 12.11.2 fixed that, one more reason not to sit on an old 12.x.
Make a node resizable with a toolbarnode-resize-toolbar
import { NodeResizer, NodeToolbar, Position } from '@xyflow/react';
function CardNode({ data, selected }) {
return (
<>
<NodeResizer isVisible={selected} minWidth={120} minHeight={60} />
<NodeToolbar isVisible={selected} position={Position.Top}>
<button onClick={data.onDelete}>delete</button>
</NodeToolbar>
<div style={{ width: '100%', height: '100%' }}>{data.label}</div>
</>
);
}Both components are in the core package in v12 (they were separate @reactflow/* packages in v11). Give the node content 100 percent width and height or the resize handles move while the visible box does not; NodeResizeControl exists for custom resize UI.
Save a flow and restore it latersave-restore
const { toObject, setViewport } = useReactFlow();
const save = () => {
localStorage.setItem('flow', JSON.stringify(toObject()));
};
const restore = () => {
const flow = JSON.parse(localStorage.getItem('flow'));
if (!flow) return;
setNodes(flow.nodes);
setEdges(flow.edges);
setViewport(flow.viewport);
};toObject returns { nodes, edges, viewport } ready for JSON. Anything non-serializable you stuffed into node data (callbacks, refs, class instances) has to be stripped before saving and rehydrated on load; the free save-and-restore example on the docs site covers this.
Keep big graphs fastlarge-graph-performance
import { memo } from 'react';
const BigNode = memo(function BigNode({ data }) {
return <div className="plain-node">{data.label}</div>;
});
const nodeTypes = { big: BigNode };
<ReactFlow
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
onlyRenderVisibleElements
/>The official performance guide in short: memo custom nodes, keep nodeTypes and defaultEdgeOptions references stable, and never subscribe side components to the whole nodes array via useNodes or a broad useStore selector, because every drag tick re-renders them. onlyRenderVisibleElements skips offscreen nodes but adds work while panning, so measure. Heavy CSS (shadows, gradients) on hundreds of nodes is often the real culprit.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| reaflow | npm | You want auto-layout (ELK) built in and accept a less flexible, more opinionated node editor |
| cytoscape | npm | Large network visualization and graph analysis with built-in layouts, where nodes are shapes rather than React components |
| mermaid | npm | You just need to render a flowchart or diagram from text, with no interactivity or editing |
| @xyflow/svelte | npm | Same library, same team, same monorepo, but your app is Svelte |