React Flow Fundamentals
Build customizable node-based editors and interactive diagrams with React Flow. This skill covers core concepts, setup, and common patterns for creating flow-based interfaces.
Installation
npm
npm install @xyflow/react
pnpm
pnpm add @xyflow/react
yarn
yarn add @xyflow/react
Basic Setup
import { useCallback } from 'react'; import { ReactFlow, useNodesState, useEdgesState, addEdge, Background, Controls, MiniMap, type Node, type Edge, type OnConnect, } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
const initialNodes: Node[] = [ { id: '1', type: 'input', data: { label: 'Start' }, position: { x: 250, y: 0 }, }, { id: '2', data: { label: 'Process' }, position: { x: 250, y: 100 }, }, { id: '3', type: 'output', data: { label: 'End' }, position: { x: 250, y: 200 }, }, ];
const initialEdges: Edge[] = [ { id: 'e1-2', source: '1', target: '2' }, { id: 'e2-3', source: '2', target: '3' }, ];
export default function Flow() { const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect: OnConnect = useCallback( (params) => setEdges((eds) => addEdge(params, eds)), [setEdges] );
return ( <div style={{ width: '100vw', height: '100vh' }}> <ReactFlow nodes={nodes} edges={edges} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onConnect={onConnect} fitView > <Background /> <Controls /> <MiniMap /> </ReactFlow> </div> ); }
Node Types
Built-in Node Types
const nodes: Node[] = [ // Input node - only has source handles { id: '1', type: 'input', data: { label: 'Input Node' }, position: { x: 0, y: 0 }, }, // Default node - has both source and target handles { id: '2', type: 'default', data: { label: 'Default Node' }, position: { x: 0, y: 100 }, }, // Output node - only has target handles { id: '3', type: 'output', data: { label: 'Output Node' }, position: { x: 0, y: 200 }, }, ];
Node Configuration
const node: Node = { id: 'unique-id', type: 'default', position: { x: 100, y: 100 }, data: { label: 'My Node', customProp: 'value' }, // Optional properties style: { backgroundColor: '#f0f0f0' }, className: 'custom-node', sourcePosition: Position.Right, targetPosition: Position.Left, draggable: true, selectable: true, connectable: true, deletable: true, hidden: false, selected: false, dragging: false, zIndex: 0, extent: 'parent', // Constrain to parent node parentId: 'parent-node-id', // For nested nodes expandParent: true, // Expand parent when node is outside bounds };
Edge Types
Built-in Edge Types
import { MarkerType } from '@xyflow/react';
const edges: Edge[] = [ // Default edge (bezier curve) { id: 'e1', source: '1', target: '2', type: 'default', }, // Straight line { id: 'e2', source: '2', target: '3', type: 'straight', }, // Step edge (right angles) { id: 'e3', source: '3', target: '4', type: 'step', }, // Smoothstep edge (rounded corners) { id: 'e4', source: '4', target: '5', type: 'smoothstep', }, ];
Edge Configuration
const edge: Edge = { id: 'edge-id', source: 'source-node-id', target: 'target-node-id', // Optional properties type: 'smoothstep', sourceHandle: 'handle-a', targetHandle: 'handle-b', label: 'Edge Label', labelStyle: { fill: '#333', fontWeight: 700 }, labelBgStyle: { fill: '#fff' }, labelBgPadding: [8, 4], labelBgBorderRadius: 4, style: { stroke: '#333', strokeWidth: 2 }, animated: true, markerEnd: { type: MarkerType.ArrowClosed, color: '#333', }, markerStart: { type: MarkerType.Arrow, }, interactionWidth: 20, deletable: true, selectable: true, selected: false, hidden: false, zIndex: 0, data: { customProp: 'value' }, };
Handles
import { Handle, Position, type NodeProps } from '@xyflow/react';
function CustomNode({ data }: NodeProps) { return ( <div className="custom-node"> {/* Target handle (input) */} <Handle type="target" position={Position.Top} id="input" style={{ background: '#555' }} isConnectable={true} />
<div>{data.label}</div>
{/* Multiple source handles */}
<Handle
type="source"
position={Position.Bottom}
id="output-a"
style={{ left: '25%', background: '#555' }}
/>
<Handle
type="source"
position={Position.Bottom}
id="output-b"
style={{ left: '75%', background: '#555' }}
/>
</div>
); }
Plugin Components
Background
import { Background, BackgroundVariant } from '@xyflow/react';
<ReactFlow nodes={nodes} edges={edges}> {/* Dots pattern */} <Background variant={BackgroundVariant.Dots} gap={12} size={1} />
{/* Lines pattern */} <Background variant={BackgroundVariant.Lines} gap={20} />
{/* Cross pattern */} <Background variant={BackgroundVariant.Cross} gap={25} />
{/* Custom styling */} <Background color="#aaa" gap={16} size={1} variant={BackgroundVariant.Dots} /> </ReactFlow>
Controls
import { Controls } from '@xyflow/react';
<ReactFlow nodes={nodes} edges={edges}> <Controls showZoom={true} showFitView={true} showInteractive={true} position="bottom-left" /> </ReactFlow>
MiniMap
import { MiniMap } from '@xyflow/react';
<ReactFlow nodes={nodes} edges={edges}> <MiniMap nodeColor={(node) => { switch (node.type) { case 'input': return '#0041d0'; case 'output': return '#ff0072'; default: return '#1a192b'; } }} nodeStrokeWidth={3} zoomable pannable /> </ReactFlow>
Panel
import { Panel } from '@xyflow/react';
<ReactFlow nodes={nodes} edges={edges}> <Panel position="top-left"> <button onClick={onSave}>Save</button> <button onClick={onRestore}>Restore</button> </Panel>
<Panel position="top-right"> <div>Node count: {nodes.length}</div> </Panel> </ReactFlow>
Event Handling
import { ReactFlow, type NodeMouseHandler, type EdgeMouseHandler, type OnSelectionChangeFunc, } from '@xyflow/react';
function Flow() { // Node events const onNodeClick: NodeMouseHandler = useCallback((event, node) => { console.log('Node clicked:', node.id); }, []);
const onNodeDoubleClick: NodeMouseHandler = useCallback((event, node) => { console.log('Node double clicked:', node.id); }, []);
const onNodeDragStart: NodeMouseHandler = useCallback((event, node) => { console.log('Drag started:', node.id); }, []);
const onNodeDrag: NodeMouseHandler = useCallback((event, node) => { console.log('Dragging:', node.position); }, []);
const onNodeDragStop: NodeMouseHandler = useCallback((event, node) => { console.log('Drag stopped:', node.position); }, []);
// Edge events const onEdgeClick: EdgeMouseHandler = useCallback((event, edge) => { console.log('Edge clicked:', edge.id); }, []);
// Selection changes const onSelectionChange: OnSelectionChangeFunc = useCallback( ({ nodes, edges }) => { console.log('Selected nodes:', nodes); console.log('Selected edges:', edges); }, [] );
return ( <ReactFlow nodes={nodes} edges={edges} onNodeClick={onNodeClick} onNodeDoubleClick={onNodeDoubleClick} onNodeDragStart={onNodeDragStart} onNodeDrag={onNodeDrag} onNodeDragStop={onNodeDragStop} onEdgeClick={onEdgeClick} onSelectionChange={onSelectionChange} /> ); }
Viewport Control
import { useReactFlow } from '@xyflow/react';
function ViewportControls() { const { zoomIn, zoomOut, fitView, setCenter, setViewport, getViewport } = useReactFlow();
return ( <div> <button onClick={() => zoomIn()}>Zoom In</button> <button onClick={() => zoomOut()}>Zoom Out</button> <button onClick={() => fitView({ padding: 0.2 })}>Fit View</button> <button onClick={() => setCenter(0, 0, { zoom: 1 })}>Center</button> <button onClick={() => { const viewport = getViewport(); console.log('Current viewport:', viewport); }} > Log Viewport </button> </div> ); }
// Must be used inside ReactFlowProvider function App() { return ( <ReactFlowProvider> <Flow /> <ViewportControls /> </ReactFlowProvider> ); }
Node Operations with useReactFlow
import { useReactFlow, type Node } from '@xyflow/react';
function NodeOperations() { const { getNodes, setNodes, getNode, addNodes, deleteElements } = useReactFlow();
const addNewNode = () => {
const newNode: Node = {
id: node-${Date.now()},
data: { label: 'New Node' },
position: { x: Math.random() * 300, y: Math.random() * 300 },
};
addNodes(newNode);
};
const updateNode = (id: string, data: object) => { setNodes((nodes) => nodes.map((node) => node.id === id ? { ...node, data: { ...node.data, ...data } } : node ) ); };
const deleteNode = (id: string) => { deleteElements({ nodes: [{ id }] }); };
const getAllNodes = () => { const nodes = getNodes(); console.log('All nodes:', nodes); };
return ( <div> <button onClick={addNewNode}>Add Node</button> <button onClick={getAllNodes}>Log Nodes</button> </div> ); }
Saving and Restoring State
import { useReactFlow, type ReactFlowJsonObject } from '@xyflow/react';
function SaveRestore() { const { toObject, setNodes, setEdges, setViewport } = useReactFlow();
const onSave = useCallback(() => { const flow = toObject(); localStorage.setItem('flow', JSON.stringify(flow)); }, [toObject]);
const onRestore = useCallback(() => { const restoreFlow = async () => { const flow = JSON.parse( localStorage.getItem('flow') || '{}' ) as ReactFlowJsonObject;
if (flow.nodes && flow.edges) {
setNodes(flow.nodes);
setEdges(flow.edges);
if (flow.viewport) {
setViewport(flow.viewport);
}
}
};
restoreFlow();
}, [setNodes, setEdges, setViewport]);
return ( <Panel position="top-right"> <button onClick={onSave}>Save</button> <button onClick={onRestore}>Restore</button> </Panel> ); }
When to Use This Skill
Use reactflow-fundamentals when you need to:
-
Build workflow builders or no-code editors
-
Create data pipeline visualizations
-
Design state machine diagrams
-
Build chatbot conversation flows
-
Create organizational charts
-
Design electrical circuit diagrams
-
Build ML pipeline visualizers
-
Create interactive decision trees
Best Practices
-
Use unique IDs for nodes and edges
-
Memoize callbacks with useCallback
-
Use TypeScript for type safety
-
Keep node components pure and performant
-
Use CSS classes instead of inline styles for complex styling
-
Store flow state in a central state manager for complex apps
-
Use fitView() on initial render for better UX
-
Add keyboard shortcuts for common operations
-
Implement undo/redo for better user experience
-
Use node validation before connections
Resources
-
React Flow Documentation
-
React Flow Examples
-
React Flow API Reference
-
React Flow GitHub