React Flow Builds Interactive Node-Based Graphs
A beginner’s guide to React Flow
React Flow is a library for building node-based graphs, which can be anything from simple static diagrams to data visualizations, to complex visual editors. It draws nodes and edges, which can be customized for various applications. It supports dragging nodes around, zooming and panning, selecting multiple nodes and edges, and adding/removing edges.
React Flow has built-in support for rendering sub-graphs and nested nodes. It supports additional functionality for MiniMap, Controls, and more. React Flow renders efficiently, because it only renders nodes that have changed, and ensures that only those that are in the viewport are rendered.
Let’s take a look at how React Flow works.
Nodes and Edges
We set up the Create React App working environment to explore React Flow.
The following command creates a React project:
% yarn create react-app react-flow-app
% cd react-flow-appInstall reactflow.
% yarn add reactflowAfter the installation, reactflow becomes part of dependencies in package.json:
"dependencies": {
"reactflow": "^11.7.4"
}React Flow draws nodes and edges:
- A node is a React component, which has a coordinate of
xandy. By default, a node is rectangle-shaped, with dimension150px X 37px. A node label is a string or a React component. A node is a type of('input' | 'output' | 'default') & CustomNodeypes. An input node can be the source of an edge, an output node can be the target of an edge, and a default node can be both source and target. The black dot is the default handle where an edge connects to the node.

- An edge connects two nodes, a target and a source nodes. An edge is a type of
DefaultEdge<T> | SmoothStepEdgeType<T> | BezierEdgeType<T>, which connects nodes in various styles.

Modify src/App.js to use reactflow:
import ReactFlow from 'reactflow';
import 'reactflow/dist/style.css';
const initialNodes = [
{ id: 'node1', position: { x: 100, y: 20 }, data: { label: 'Node 1' } },
{ id: 'node2', position: { x: 100, y: 120 }, data: { label: 'Node 2' } },
];
const initialEdges = [
{ id: 'edge-node1-node2', source: 'node1', target: 'node2' },
];
function App() {
return (
<div style={{ width: '90vw', height: '90vh' }}>
<ReactFlow nodes={initialNodes} edges={initialEdges} />
</div>
);
}
export default App;reactflow/dist/style.css, or a custom theme, must be imported to show nodes and edges.- The
<ReactFlow />component must be wrapped in an element with width and height. <ReactFlow nodes={initialNodes} edges={initialEdges} />includes two initial nodes,Node 1andNode 2, which are connected by an initial edge.
Execute yarn start, and we see the graph.

There is an attribution label at the bottom-right corner, which can be removed by the following prop:
proOptions={{ hideAttribution: true }}However, for commercial use, it is required to show the attribution or sign up for one of the subscriptions.
There is also a fitView prop that automatically fits the graph into the viewport:
<ReactFlow
nodes={initialNodes}
edges={initialEdges}
proOptions={{ hideAttribution: true }}
fitView
/>Here is the fitted graph without the attribution:

Controlled and Uncontrolled Flow
Controlled and uncontrolled components are two important concepts in React:
- A controlled component manages its children’s data.
- An uncontrolled component lets its children manage their own data.
There are also two ways to use React Flow — controlled or uncontrolled. Controlled means the application is in control of the state of the nodes and edges.
The following is an example of the controlled flow:
import ReactFlow from 'reactflow';
import 'reactflow/dist/style.css';
const initialNodes = [
{ id: 'node1', position: { x: 100, y: 20 }, data: { label: 'Node 1' } },
{ id: 'node2', position: { x: 100, y: 120 }, data: { label: 'Node 2' } },
];
function App() {
return (
<div style={{ width: '90vw', height: '90vh' }}>
<ReactFlow nodes={initialNodes} edges={[]} />
</div>
);
}
export default App;Without programming the interactivity, we can pan the graph while dragging the mouse, or zoom the graph while scrolling the mouse. But we cannot connect two nodes.

In an uncontrolled flow, the state of the nodes and edges is handled by React Flow internally.
The following is an example of the uncontrolled flow:
import ReactFlow from 'reactflow';
import 'reactflow/dist/style.css';
const initialNodes = [
{ id: 'node1', position: { x: 100, y: 20 }, data: { label: 'Node 1' } },
{ id: 'node2', position: { x: 100, y: 120 }, data: { label: 'Node 2' } },
];
function App() {
return (
<div style={{ width: '90vw', height: '90vh' }}>
<ReactFlow defaultNodes={initialNodes} defaultEdges={[]} />
</div>
);
}
export default App;We do not need further programming, and the connection just works out of the box.

The uncontrolled flow seems simpler, but it only works for simple use cases. In most of situations, the controlled flow is recommended. The examples in the remaining article adopt the controlled React Flow.
React Flow With Interactivity
For the controlled flow, the interactivity needs to be implemented. Typically, they are implemented by onNodesChange and onEdgesChange handlers:
onNodesChange: (nodeChanges: NodeChange[]) => void: It is called when nodes are dragged, selected, added, removed, etc., whereNodeChangeis defined as follows:
export type NodeDimensionChange = {
id: string;
type: 'dimensions';
dimensions?: Dimensions;
updateStyle?: boolean;
resizing?: boolean;
};
export type NodePositionChange = {
id: string;
type: 'position';
position?: XYPosition;
positionAbsolute?: XYPosition;
dragging?: boolean;
};
export type NodeSelectionChange = {
id: string;
type: 'select';
selected: boolean;
};
export type NodeRemoveChange = {
id: string;
type: 'remove';
};
export type NodeAddChange<NodeData = any> = {
item: Node<NodeData>;
type: 'add';
};
export type NodeResetChange<NodeData = any> = {
item: Node<NodeData>;
type: 'reset';
};
export type NodeChange = NodeDimensionChange | NodePositionChange | NodeSelectionChange | NodeRemoveChange | NodeAddChange | NodeResetChange;onEdgesChange: (edgeChanges: EdgeChange[]) => void: It is called when edges are selected, added, removed, etc., whereEdgeChangeis defined as follows:
export type EdgeSelectionChange = NodeSelectionChange;
export type EdgeRemoveChange = NodeRemoveChange;
export type EdgeAddChange<EdgeData = any> = {
item: Edge<EdgeData>;
type: 'add';
};
export type EdgeResetChange<EdgeData = any> = {
item: Edge<EdgeData>;
type: 'reset';
};
export type EdgeChange = EdgeSelectionChange | EdgeRemoveChange | EdgeAddChange | EdgeResetChange;Here is the modified src/App.js:
import { useCallback, useState } from 'react';
import ReactFlow, { applyEdgeChanges, applyNodeChanges } from 'reactflow';
import 'reactflow/dist/style.css';
const initialNodes = [
{ id: 'node1', position: { x: 100, y: 20 }, data: { label: 'Node 1' } },
{ id: 'node2', position: { x: 100, y: 120 }, data: { label: 'Node 2' } },
];
const initialEdges = [
{ id: 'edge-node1-node2', source: 'node1', target: 'node2' },
];
function App() {
const [nodes, setNodes] = useState(initialNodes);
const [edges, setEdges] = useState(initialEdges);
const onNodesChange = useCallback(
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
[setNodes]
);
const onEdgesChange = useCallback(
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
[setEdges]
);
return (
<div style={{ width: '90vw', height: '90vh' }}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
/>
</div>
);
}
export default App;Execute yarn start, and we can interact with the graph.

The interactivity includes:
- Selectable nodes and edges
- Draggable nodes
- Removable nodes and edges using Backspace
- Multi-selection area by pressing Shift + mouse action
- Multi-selection by pressing Command + mouse action
For convenience, we can use helper hooks, useNodesState and useEdgesState, to create the state of nodes and edges:
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);In addition, we add the capability to connect two nodes with the prop, onConnect:
onConnect: (connection: Connection) => void: It is called when a user connects two nodes in a controlled flow, whereConnectionis defined as follows:
export interface Connection {
source: string | null;
target: string | null;
sourceHandle: string | null;
targetHandle: string | null;
}Here is the modified src/App.js:
import { useCallback } from 'react';
import ReactFlow, { useNodesState, useEdgesState, addEdge } from 'reactflow';
import 'reactflow/dist/style.css';
const initialNodes = [
{ id: 'node1', position: { x: 100, y: 20 }, data: { label: 'Node 1' } },
{ id: 'node2', position: { x: 100, y: 120 }, data: { label: 'Node 2' } },
{ id: 'node3', position: { x: 100, y: 220 }, data: { label: 'Node 3' } },
];
const initialEdges = [
{ id: 'edge-node1-node2', source: 'node1', target: 'node2' },
];
function App() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback(
(params) => setEdges((eds) => addEdge(params, eds)),
[setEdges]
);
return (
<div style={{ width: '90vw', height: '90vh' }}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
/>
</div>
);
}
export default App;Execute yarn start, and we can connect two nodes.

For the newly added edge, we can turn on animation:
const onConnect = useCallback(
(connection) => setEdges((eds) => addEdge({ ...connection, animated: true }, eds)),
[setEdges]
);Execute yarn start, and the newly added edge is animated.

If we set defaultEdgeOptions animated, all edges will be animated.
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
defaultEdgeOptions={{ animated: true }}
/>Sub Flows
A sub flow is a flow inside a node. It can be a separate flow or a flow that is connected with other nodes outside of its parent. This feature can also be used for grouping nodes.
Here is the modified src/App.js that has two groups that are connected:
import ReactFlow from 'reactflow';
import 'reactflow/dist/style.css';
const initialNodes = [
{
id: 'parent1',
type: 'group',
position: { x: 100, y: 20 },
style: { width: 190, height: 180, backgroundColor: 'yellow' },
},
{
id: 'node1',
type: 'input',
position: { x: 20, y: 20 },
data: { label: 'Node 1' },
parentNode: 'parent1',
extent: 'parent',
},
{
id: 'node2',
type: 'default',
position: { x: 20, y: 120 },
data: { label: 'Node 2' },
parentNode: 'parent1',
extent: 'parent',
},
{
id: 'parent2',
type: 'group',
position: { x: 300, y: 300 },
style: { width: 190, height: 180, backgroundColor: 'pink' },
},
{
id: 'nodeA',
type: 'default',
position: { x: 20, y: 20 },
data: { label: 'Node A' },
parentNode: 'parent2',
extent: 'parent',
},
{
id: 'nodeB',
type: 'output',
position: { x: 20, y: 120 },
data: { label: 'Node B' },
parentNode: 'parent2',
extent: 'parent',
},
];
const initialEdges = [
{ id: 'edge-node1-node2', source: 'node1', target: 'node2' },
{ id: 'edge-nodeA-nodeB', source: 'nodeA', target: 'nodeB' },
{ id: 'edge-nodeA-nodeB', source: 'node2', target: 'nodeA' },
];
function App() {
return (
<div style={{ width: '90vw', height: '90vh' }}>
<ReactFlow
nodes={initialNodes}
edges={initialEdges}
/>
</div>
);
}
export default App;Execute yarn start, and we see two grouped nodes. But, some edges are missing.

This can be fixed to set nodes’ z-index to -1 in src/index.css:
.react-flow__node {
z-index: -1 !important;
}Now two groups are displayed correctly.

Add Node On Edge Drop
We have added an edge to connect nodes. How do we add a node?
We are going to use the hook, useReactFlow, which has the following functions:
- Manipulate nodes, edges, and the viewport.
- Get information about the current state.
export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlowInstance<NodeData, EdgeData>;useReactFlow can only be used if the component that uses it, is wrapped with a ReactFlowProvider or if it is a child of the <ReactFlow /> component.
Here is the definition of ReactFlowInstance, which has many useful methods to manipulate the graph:
export type ReactFlowInstance<NodeData = any, EdgeData = any> = {
getNodes: Instance.GetNodes<NodeData>;
addNodes: Instance.AddNodes<NodeData>;
getNode: Instance.GetNode<NodeData>;
getEdges: Instance.GetEdges<EdgeData>;
setEdges: Instance.SetEdges<EdgeData>;
addEdges: Instance.AddEdges<EdgeData>;
getEdge: Instance.GetEdge<EdgeData>;
toObject: Instance.ToObject<NodeData, EdgeData>;
deleteElements: Instance.DeleteElements;
getIntersectingNodes: Instance.GetIntersectingNodes<NodeData>;
isNodeIntersecting: Instance.IsNodeIntersecting<NodeData>;
viewportInitialized: boolean;
} & Omit<ViewportHelperFunctions, 'initialized'>;In addition, ViewportHelperFunctions is defined as follows:
export type ViewportHelperFunctions = {
zoomIn: ZoomInOut;
zoomOut: ZoomInOut;
zoomTo: ZoomTo;
getZoom: GetZoom;
setViewport: SetViewport;
getViewport: GetViewport;
fitView: FitView;
setCenter: SetCenter;
fitBounds: FitBounds;
project: Project;
viewportInitialized: boolean;
};Among all viewport helpers, Project is a method that can be used to set a position:
export type Project = (position: XYPosition) => XYPosition;Here is the modified src/App.js:
import React, { useCallback, useRef } from 'react';
import ReactFlow, {
useNodesState,
useEdgesState,
addEdge,
useReactFlow,
ReactFlowProvider,
} from 'reactflow';
import 'reactflow/dist/style.css';
const initialNodes = [
{ id: 'node1', position: { x: 100, y: 20 }, data: { label: 'Node 1' } },
];
function AddNodeOnEdgeDrop() {
const reactFlowWrapper = useRef(null);
const connectingNodeId = useRef(null);
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const { project } = useReactFlow();
const onConnect = useCallback(
(params) => setEdges((eds) => addEdge(params, eds)),
[setEdges]
);
const onConnectStart = useCallback((_, { nodeId }) => {
connectingNodeId.current = nodeId;
}, []);
const onConnectEnd = useCallback(
(event) => {
const targetIsPane = event.target.classList.contains('react-flow__pane');
if (targetIsPane) {
// remove the wrapper bounds to get the correct position od the top left
const { top, left } = reactFlowWrapper.current.getBoundingClientRect();
const id = `node${nodes.length + 1}`; // node id
const newNode = {
id,
position: project({ // project the position
x: event.clientX - left - 75, // 75 is half of the width 150
y: event.clientY - top,
}),
data: { label: `Node ${nodes.length + 1}` },
};
// add new node
setNodes((nds) => nds.concat(newNode));
// add new edge
setEdges((eds) =>
eds.concat({ id, source: connectingNodeId.current, target: id })
);
}
},
[project, nodes, setEdges, setNodes]
);
return (
<div style={{ width: '90vw', height: '90vh' }} ref={reactFlowWrapper}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onConnectStart={onConnectStart}
onConnectEnd={onConnectEnd}
/>
</div>
);
}
function App() {
return (
<ReactFlowProvider>
<AddNodeOnEdgeDrop />
</ReactFlowProvider>
);
}
export default App;Execute yarn start, and we can add node on edge drop:

Save and Restore Graph
We can drag and drop nodes and edges, panning and zooming. Can we save the modified graph?
Yes, we can.
Panel is a helper component to position content on top of the React Flow viewport. We add two buttons on the panel, Save Diagram and Restore Diagram. The graph information is saved in the local storage.
Here is the modified src/App.js:
import React, { useCallback, useRef, useState } from 'react';
import ReactFlow, {
useNodesState,
useEdgesState,
addEdge,
useReactFlow,
Panel,
ReactFlowProvider,
} from 'reactflow';
import 'reactflow/dist/style.css';
const initialNodes = [
{ id: 'node1', position: { x: 100, y: 20 }, data: { label: 'Node 1' } },
];
function AddNodeOnEdgeDrop() {
const reactFlowWrapper = useRef(null);
const connectingNodeId = useRef(null);
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const [reactFlowInstance, setReactFlowInstance] = useState(null);
const { project, setViewport } = useReactFlow();
const onConnect = useCallback(
(params) => setEdges((eds) => addEdge(params, eds)),
[setEdges]
);
const onConnectStart = useCallback((_, { nodeId }) => {
connectingNodeId.current = nodeId;
}, []);
const onConnectEnd = useCallback(
(event) => {
const targetIsPane = event.target.classList.contains('react-flow__pane');
if (targetIsPane) {
// remove the wrapper bounds to get the correct position od the top left
const { top, left } = reactFlowWrapper.current.getBoundingClientRect();
const id = `node${nodes.length + 1}`; // node id
const newNode = {
id,
position: project({
// project the position
x: event.clientX - left - 75, // 75 is half of the width 150
y: event.clientY - top,
}),
data: { label: `Node ${nodes.length + 1}` },
};
// add new node
setNodes((nds) => nds.concat(newNode));
// add new edge
setEdges((eds) =>
eds.concat({ id, source: connectingNodeId.current, target: id })
);
}
},
[project, nodes, setEdges, setNodes]
);
// save the current graph
const onSave = useCallback(() => {
if (reactFlowInstance) {
const flow = reactFlowInstance.toObject();
localStorage.setItem('my-flow-data', JSON.stringify(flow));
}
}, [reactFlowInstance]);
// restore the last saved graph
const onRestore = useCallback(() => {
const restoreFlow = async () => {
const flow = JSON.parse(localStorage.getItem('my-flow-data'));
if (flow) {
const { x = 0, y = 0, zoom = 1 } = flow.viewport;
setNodes(flow.nodes ?? []);
setEdges(flow.edges ?? []);
setViewport({ x, y, zoom });
}
};
restoreFlow();
}, [setNodes, setEdges, setViewport]);
return (
<div style={{ width: '90vw', height: '90vh' }} ref={reactFlowWrapper}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onConnectStart={onConnectStart}
onConnectEnd={onConnectEnd}
onInit={setReactFlowInstance}
>
<Panel position="top-right" style={{ display: 'flex', gap: '10px' }}>
<button onClick={onSave}>Save Diagram</button>
<button onClick={onRestore}>Restore Diagram</button>
</Panel>
</ReactFlow>
</div>
);
}
function App() {
return (
<ReactFlowProvider>
<AddNodeOnEdgeDrop />
</ReactFlowProvider>
);
}
export default App;Execute yearn start, and we can save a diagram and restore it later.









