mrkeyoor.com_
Thu 06 Aug 23:56 UTC
npmWeb Frontendupdated 06 Aug 2026

@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.

Verdict

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.

API stability4/5v12 (the @xyflow/react rename) has been the stable line since 2024 with only additive minors since (12.11.0 added autoPanOnSelection in June 2026). The point off is the migration history: reactflow to @xyflow/react renamed project() to screenToFlowPosition, parentNode to parentId, and moved dimensions to node.measured, so most old community code is wrong.
Docs5/5reactflow.dev has learn guides, a full API reference, dedicated performance and layouting guides, TypeScript docs, and llms.txt plus llms-full.txt for AI tools. The honest caveat: a meaningful slice of the examples gallery (auto layout, undo/redo, collaborative, lasso) is paywalled as Pro examples.
Maintenance5/5Repo pushed 2026-08-06, releases roughly monthly (12.11.2 on 2026-07-06, 12.11.1 in June, 12.11.0 with new features in June), maintained by the xyflow company funded through Pro subscriptions. 138 open issues counting PRs on a 37.9k star monorepo is well managed.
Ecosystem5/510.2M weekly npm downloads, shadcn-style React Flow UI components, a Liveblocks SDK for realtime collaboration, community packages like react-flow-smart-edge, and it is the canvas under a large share of current AI workflow products.

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
Skip it if

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

PackageRegistryPick it when
reaflownpmYou want auto-layout (ELK) built in and accept a less flexible, more opinionated node editor
cytoscapenpmLarge network visualization and graph analysis with built-in layouts, where nodes are shapes rather than React components
mermaidnpmYou just need to render a flowchart or diagram from text, with no interactivity or editing
@xyflow/sveltenpmSame library, same team, same monorepo, but your app is Svelte