React Flow 是一个用于构建节点式界面的 React 库,常见场景包括工作流编排、低代码编辑器、数据血缘图、AI Agent 流程和状态机编辑器。
文章从一个最小画布开始,依次介绍节点、连线、自定义节点、自定义边和布局。目标是先建立可运行的结构,再补充真实项目会遇到的细节。
安装
安装依赖:
npm install @xyflow/react现在官方包名是 @xyflow/react。旧资料里常见的 reactflow 包名仍然能搜到,新项目建议直接使用 @xyflow/react。
在全局样式中引入 React Flow 的 CSS:
@import "tailwindcss";
@import "@xyflow/react/dist/style.css";如果项目使用 Tailwind CSS 4,React Flow 的样式需要放在 Tailwind 后面。官方 Quick Start 也建议把这段样式放在 global.css 或 index.css 中,而不是放在业务组件里。
还需要给画布父容器设置明确宽高。ReactFlow 会填满父容器,父容器没有高度时,页面会显示为空白。
最小示例
React Flow 的核心数据是 nodes 和 edges:
nodes表示画布上的节点edges表示节点之间的连线
'use client'
import { ReactFlow } from '@xyflow/react'
const nodes = [
{
id: 'start',
position: { x: 0, y: 0 },
data: { label: '开始' },
},
{
id: 'review',
position: { x: 260, y: 120 },
data: { label: '人工审核' },
},
]
const edges = [
{
id: 'start-review',
source: 'start',
target: 'review',
},
]
export default function App() {
return (
<div className='not-prose overflow-hidden rounded-md border border-zinc-200 bg-white text-zinc-950 shadow-sm'>
<div className='h-[420px] w-full'>
<ReactFlow
className='bg-white text-zinc-950'
edges={edges}
fitView
nodes={nodes}
/>
</div>
</div>
)
}
position 是节点在画布中的坐标。data.label 会被默认节点用作显示文本。edge.source 和 edge.target 对应两个节点的 id。
这段代码可以渲染一张基础流程图,但节点拖拽、删除、连线等操作还不会写回数据。
受控状态
要让画布成为可编辑界面,需要把 React Flow 产生的变化写回状态。
官方 Quick Start 使用 applyNodeChanges、applyEdgeChanges 和 addEdge。如果只是管理本地状态,useNodesState 和 useEdgesState 更直接。
'use client'
import {
addEdge,
ReactFlow,
useEdgesState,
useNodesState,
type Connection,
type Edge,
type Node,
} from '@xyflow/react'
import { useCallback } from 'react'
const initialNodes: Node[] = [
{ id: 'input', position: { x: 0, y: 0 }, data: { label: '输入' } },
{ id: 'output', position: { x: 280, y: 120 }, data: { label: '输出' } },
]
const initialEdges: Edge[] = [
{ id: 'input-output', source: 'input', target: 'output' },
]
export function ControlledFlow() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes)
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges)
const onConnect = useCallback(
(connection: Connection) => {
setEdges((currentEdges) => addEdge(connection, currentEdges))
},
[setEdges]
)
return (
<div style={{ height: 420 }}>
<ReactFlow
edges={edges}
nodes={nodes}
onConnect={onConnect}
onEdgesChange={onEdgesChange}
onNodesChange={onNodesChange}
fitView
/>
</div>
)
}这段代码处理了三件事:
onNodesChange写回节点拖拽、选择、删除等变化onEdgesChange写回连线变化onConnect在用户新建连接时添加一条边
后续接入 Zustand、Redux 或服务端同步时,也是在这几个事件上扩展。
内置组件
React Flow 提供了一些常用画布组件:
Background:网格背景Controls:缩放、适配视图、锁定交互MiniMap:小地图Panel:固定在画布角落的容器
import { Background, Controls, MiniMap, ReactFlow } from '@xyflow/react'
export function FlowWithTools() {
return (
<div style={{ height: 420 }}>
<ReactFlow edges={edges} nodes={nodes} fitView>
<Background />
<MiniMap pannable zoomable />
<Controls />
</ReactFlow>
</div>
)
}这些组件需要放在 ReactFlow 内部。它们依赖画布上下文,不需要额外传入 nodes 或 edges。
下面是一个完整一点的示例:包含自定义节点、连线、背景、小地图和控制按钮。
自定义节点
默认节点适合快速验证。真实项目通常需要自定义节点,例如在节点里显示状态、表单项、运行结果或错误信息。
自定义节点的步骤:
- 写一个 React 组件
- 用
nodeTypes注册组件 - 在节点数据中设置对应的
type
import {
Handle,
Position,
type Node,
type NodeProps,
type NodeTypes,
} from '@xyflow/react'
type WorkflowNodeData = {
title: string
description: string
status: 'Ready' | 'Running' | 'Failed'
}
type WorkflowNode = Node<WorkflowNodeData, 'workflow'>
function WorkflowNodeCard({ data }: NodeProps<WorkflowNode>) {
return (
<div className='rounded-md border bg-background px-4 py-3 shadow-sm'>
<Handle type='target' position={Position.Left} />
<div className='font-medium'>{data.title}</div>
<p className='text-muted-foreground text-sm'>{data.description}</p>
<Handle type='source' position={Position.Right} />
</div>
)
}
export const nodeTypes = {
workflow: WorkflowNodeCard,
} satisfies NodeTypes节点数据:
const nodes: WorkflowNode[] = [
{
id: 'draft',
type: 'workflow',
position: { x: 0, y: 0 },
data: {
title: '写草稿',
description: '整理资料,确定文章结构。',
status: 'Ready',
},
},
]传给画布:
<ReactFlow nodeTypes={nodeTypes} nodes={nodes} edges={edges} />nodeTypes 建议放在组件外部,或通过 useMemo 保持引用稳定。频繁创建新的 nodeTypes 对象会触发不必要的更新,也可能出现开发环境警告。
节点数据可以分成两类:
- 画布数据:
position、selected、dragging - 业务数据:放在
data中,例如名称、接口地址、分支条件、重试次数
不要把完整后端对象直接塞进 data。节点数据会参与渲染、保存、撤销重做和导入导出。保持 data 简洁,后续维护成本更低。
建议遵守几条规则:
id使用稳定 ID,不使用数组下标data只放节点渲染和编辑需要的数据- 大对象、请求缓存、临时运行结果放到外部 store
- 持久化时把
nodes和edges当作图结构快照
Handle
Handle 是节点上的连接点。边从 source 类型的 Handle 出来,连接到 target 类型的 Handle。
<Handle type='target' position={Position.Left} />
<Handle type='source' position={Position.Right} />常见布局是左侧输入、右侧输出。这样和从左到右的流程方向一致,也方便后续做横向布局。
一个节点可以有多个 Handle。比如任务节点有“成功”和“失败”两个出口:
<Handle id='success' type='source' position={Position.Right} />
<Handle id='error' type='source' position={Position.Bottom} />边可以指定连接到哪个 Handle:
const edges = [
{
id: 'task-error',
source: 'task',
sourceHandle: 'error',
target: 'fallback',
},
]这类字段在工作流编辑器里很常见。它不仅说明两个节点相连,还说明连接来自哪个分支。
如果 Handle 会动态增删,需要调用 useUpdateNodeInternals()。它会通知 React Flow 重新计算节点内部连接点的位置。
边
最基础的边只需要 id、source 和 target:
const edges = [
{
id: 'a-b',
source: 'a',
target: 'b',
},
]真实项目里的边通常还会包含类型、标签、动画、箭头和业务状态:
const edges = [
{
id: 'review-pass',
source: 'review',
sourceHandle: 'pass',
target: 'publish',
type: 'smoothstep',
animated: true,
label: '通过',
},
]常用内置边类型:
default:贝塞尔曲线straight:直线step:折线smoothstep:圆滑折线
如果只需要展示文本,使用 label 即可。如果需要在线上放按钮、状态标记或菜单,需要自定义边。
自定义边通常由三部分组成:
- 路径工具,例如
getBezierPath BaseEdge,负责渲染 SVG pathEdgeLabelRenderer,负责渲染 HTML 标签
import {
BaseEdge,
EdgeLabelRenderer,
getBezierPath,
type EdgeProps,
} from '@xyflow/react'
function LabeledEdge(props: EdgeProps) {
const [edgePath, labelX, labelY] = getBezierPath(props)
return (
<>
<BaseEdge id={props.id} path={edgePath} />
<EdgeLabelRenderer>
<div
className='nodrag nopan'
style={{
position: 'absolute',
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
}}
>
审核通过
</div>
</EdgeLabelRenderer>
</>
)
}nodrag 和 nopan 用于避免标签区域触发画布拖拽或平移。边标签中包含按钮、菜单或输入框时,建议加上。
下面的示例展示了自定义边和边标签。
连接规则也应该单独处理。比如结束节点不能继续连出,错误分支只能连到兜底节点。
简单规则可以写在 isValidConnection:
<ReactFlow
isValidConnection={(connection) => {
return connection.source !== connection.target
}}
/>规则复杂时,可以抽成 canConnect(nodes, edges, connection),并在拖线、导入、保存前校验中复用。
布局
React Flow 不内置自动布局。节点的位置由 position 决定,需要应用自己计算和保存。
常见方案有三种:
- 保存用户拖拽后的坐标
- 新建节点时根据上下文给默认位置
- 使用布局库生成节点坐标
对于流程编辑器,通常需要保存用户坐标。自动布局可以作为“整理画布”功能提供,不适合在每次数据变化后强制执行。
官方 Layouting 文档列出了几类常用布局库:
- dagre:适合有方向的流程图或 DAG,上手成本低
- d3-hierarchy:适合单根树结构
- d3-force:适合关系网络,需要处理计算时机
- elkjs:配置能力强,适合复杂图、子流程、多 Handle 和边路由
下面的示例没有引入布局库,只用两组固定坐标模拟横向和纵向布局。布局算法最终要做的事情也是一样:生成新的 position,再写回 nodes。
简化后的代码结构:
function getLayoutNodes(direction: 'LR' | 'TB'): Node[] {
return nodes.map((node) => ({
...node,
position: calculatePosition(node, direction),
sourcePosition: direction === 'LR' ? Position.Right : Position.Bottom,
targetPosition: direction === 'LR' ? Position.Left : Position.Top,
}))
}
setNodes(getLayoutNodes('LR'))接入 dagre 或 elkjs 后,流程仍然类似:把 nodes 和 edges 传给布局库,拿回坐标,更新节点。区别在于布局库通常需要节点尺寸,有些计算过程是异步的。
布局逻辑建议独立成文件:
flow-editor/
canvas.tsx
layout.ts
nodes/
edges/
panels/
types.ts这样后续更换布局算法时,不需要改动主要画布组件。
ReactFlowProvider
如果工具按钮、状态栏和面板都写在 ReactFlow 内部,通常不需要额外处理。它们已经在画布上下文中。
当工具栏、属性面板或右键菜单被拆到画布外部,并且需要调用 useReactFlow() 时,需要使用 ReactFlowProvider。
import { ReactFlowProvider } from '@xyflow/react'
export function FlowEditorPage() {
return (
<ReactFlowProvider>
<FlowCanvas />
<InspectorPanel />
</ReactFlowProvider>
)
}外部组件可以获取画布实例:
import { useReactFlow } from '@xyflow/react'
function InspectorPanel() {
const { fitView, getNodes } = useReactFlow()
return (
<button onClick={() => console.log(getNodes())} type='button'>
打印节点
</button>
)
}项目组织
流程编辑器通常会持续增加功能。建议按职责拆分文件:
canvas.tsx 负责组装 ReactFlow。节点组件放在 nodes/,边组件放在 edges/,属性面板和工具栏放在 panels/。
状态管理可以从简单方案开始。示例和小型编辑器使用 useNodesState、useEdgesState 就够了。需要撤销重做、草稿保存、服务端校验或协作时,再抽到外部状态管理。
常见问题
没有引入 CSS。缺少 @xyflow/react/dist/style.css 时,节点、Handle、Controls 的样式会不完整。
父容器没有高度。ReactFlow 不会自己撑开高度,调试时可以先固定为 height: 420。
在组件内部重复创建 nodeTypes 和 edgeTypes。应该放在组件外部,或使用 useMemo。
节点里有输入框但没有加 nodrag。输入框、选择器、按钮等交互元素需要避免触发节点拖拽。
把大对象放进 data。这会影响渲染、持久化和撤销重做。
自动布局频繁重排。用户手动调整过的位置应该保存,自动布局适合作为显式操作。
学习路径
- 用
nodes和edges渲染最小画布 - 用
useNodesState、useEdgesState接管交互 - 用
addEdge实现连线 - 加入
Background、Controls、MiniMap - 使用
nodeTypes自定义节点 - 使用
Handle设计输入输出口 - 使用
edgeTypes自定义边和标签 - 保存用户坐标,再按需要接入布局库
- 使用
ReactFlowProvider连接外部工具栏和面板 - 处理持久化、撤销重做、协作和性能
React Flow 的基础用法不复杂。更需要设计的是图结构和编辑器行为:节点数据如何拆分,连接规则如何校验,布局什么时候触发,用户坐标如何保存,错误状态显示在节点上还是边上。
