React Flow 上手指南
2026年06月10日

React Flow 上手指南

从最小示例开始,介绍 React Flow 的节点、连线、自定义组件、布局和项目组织方式。

React Flow 是一个用于构建节点式界面的 React 库,常见场景包括工作流编排、低代码编辑器、数据血缘图、AI Agent 流程和状态机编辑器。

文章从一个最小画布开始,依次介绍节点、连线、自定义节点、自定义边和布局。目标是先建立可运行的结构,再补充真实项目会遇到的细节。

安装

安装依赖:

npm install @xyflow/react

现在官方包名是 @xyflow/react。旧资料里常见的 reactflow 包名仍然能搜到,新项目建议直接使用 @xyflow/react

在全局样式中引入 React Flow 的 CSS:

globals.css
@import "tailwindcss";
@import "@xyflow/react/dist/style.css";

如果项目使用 Tailwind CSS 4,React Flow 的样式需要放在 Tailwind 后面。官方 Quick Start 也建议把这段样式放在 global.cssindex.css 中,而不是放在业务组件里。

还需要给画布父容器设置明确宽高。ReactFlow 会填满父容器,父容器没有高度时,页面会显示为空白。

最小示例

React Flow 的核心数据是 nodesedges

  • nodes 表示画布上的节点
  • edges 表示节点之间的连线
App.tsx
'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.sourceedge.target 对应两个节点的 id

这段代码可以渲染一张基础流程图,但节点拖拽、删除、连线等操作还不会写回数据。

受控状态

要让画布成为可编辑界面,需要把 React Flow 产生的变化写回状态。

官方 Quick Start 使用 applyNodeChangesapplyEdgeChangesaddEdge。如果只是管理本地状态,useNodesStateuseEdgesState 更直接。

ControlledFlow.tsx
'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 内部。它们依赖画布上下文,不需要额外传入 nodesedges

下面是一个完整一点的示例:包含自定义节点、连线、背景、小地图和控制按钮。

React Flow 工作流示例
节点可拖拽,右侧 Handle 可以继续连线。
Mini Map

自定义节点

默认节点适合快速验证。真实项目通常需要自定义节点,例如在节点里显示状态、表单项、运行结果或错误信息。

自定义节点的步骤:

  1. 写一个 React 组件
  2. nodeTypes 注册组件
  3. 在节点数据中设置对应的 type
WorkflowNode.tsx
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 对象会触发不必要的更新,也可能出现开发环境警告。

节点数据可以分成两类:

  • 画布数据:positionselecteddragging
  • 业务数据:放在 data 中,例如名称、接口地址、分支条件、重试次数

不要把完整后端对象直接塞进 data。节点数据会参与渲染、保存、撤销重做和导入导出。保持 data 简洁,后续维护成本更低。

建议遵守几条规则:

  • id 使用稳定 ID,不使用数组下标
  • data 只放节点渲染和编辑需要的数据
  • 大对象、请求缓存、临时运行结果放到外部 store
  • 持久化时把 nodesedges 当作图结构快照

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 重新计算节点内部连接点的位置。

最基础的边只需要 idsourcetarget

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 path
  • EdgeLabelRenderer,负责渲染 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>
    </>
  )
}

nodragnopan 用于避免标签区域触发画布拖拽或平移。边标签中包含按钮、菜单或输入框时,建议加上。

下面的示例展示了自定义边和边标签。

自定义边和边标签
这条线不是普通 SVG path,还带有可以交互的标签层。

连接规则也应该单独处理。比如结束节点不能继续连出,错误分支只能连到兜底节点。

简单规则可以写在 isValidConnection

<ReactFlow
  isValidConnection={(connection) => {
    return connection.source !== connection.target
  }}
/>

规则复杂时,可以抽成 canConnect(nodes, edges, connection),并在拖线、导入、保存前校验中复用。

布局

React Flow 不内置自动布局。节点的位置由 position 决定,需要应用自己计算和保存。

常见方案有三种:

  1. 保存用户拖拽后的坐标
  2. 新建节点时根据上下文给默认位置
  3. 使用布局库生成节点坐标

对于流程编辑器,通常需要保存用户坐标。自动布局可以作为“整理画布”功能提供,不适合在每次数据变化后强制执行。

官方 Layouting 文档列出了几类常用布局库:

  • dagre:适合有方向的流程图或 DAG,上手成本低
  • d3-hierarchy:适合单根树结构
  • d3-force:适合关系网络,需要处理计算时机
  • elkjs:配置能力强,适合复杂图、子流程、多 Handle 和边路由

下面的示例没有引入布局库,只用两组固定坐标模拟横向和纵向布局。布局算法最终要做的事情也是一样:生成新的 position,再写回 nodes

布局切换示例
这里用固定坐标模拟布局结果,真实项目可以换成 dagre 或 elkjs。
Mini Map

简化后的代码结构:

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 后,流程仍然类似:把 nodesedges 传给布局库,拿回坐标,更新节点。区别在于布局库通常需要节点尺寸,有些计算过程是异步的。

布局逻辑建议独立成文件:

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
flow-state.ts
layout.ts
types.ts
task-node.tsx
branch-node.tsx
note-node.tsx
labeled-edge.tsx
condition-edge.tsx
inspector.tsx
toolbar.tsx

canvas.tsx 负责组装 ReactFlow。节点组件放在 nodes/,边组件放在 edges/,属性面板和工具栏放在 panels/

状态管理可以从简单方案开始。示例和小型编辑器使用 useNodesStateuseEdgesState 就够了。需要撤销重做、草稿保存、服务端校验或协作时,再抽到外部状态管理。

常见问题

没有引入 CSS。缺少 @xyflow/react/dist/style.css 时,节点、Handle、Controls 的样式会不完整。

父容器没有高度。ReactFlow 不会自己撑开高度,调试时可以先固定为 height: 420

在组件内部重复创建 nodeTypesedgeTypes。应该放在组件外部,或使用 useMemo

节点里有输入框但没有加 nodrag。输入框、选择器、按钮等交互元素需要避免触发节点拖拽。

把大对象放进 data。这会影响渲染、持久化和撤销重做。

自动布局频繁重排。用户手动调整过的位置应该保存,自动布局适合作为显式操作。

学习路径

  1. nodesedges 渲染最小画布
  2. useNodesStateuseEdgesState 接管交互
  3. addEdge 实现连线
  4. 加入 BackgroundControlsMiniMap
  5. 使用 nodeTypes 自定义节点
  6. 使用 Handle 设计输入输出口
  7. 使用 edgeTypes 自定义边和标签
  8. 保存用户坐标,再按需要接入布局库
  9. 使用 ReactFlowProvider 连接外部工具栏和面板
  10. 处理持久化、撤销重做、协作和性能

React Flow 的基础用法不复杂。更需要设计的是图结构和编辑器行为:节点数据如何拆分,连接规则如何校验,布局什么时候触发,用户坐标如何保存,错误状态显示在节点上还是边上。

参考资料