React Flow 实战(三)—— 使用 React.context 管理流程图数据


前面两篇关于 React Flow 的文章已经介绍了如何绘制流程图

而实际项目中,流程图上的每一个节点,甚至每一条连线都需要维护一份独立的业务数据

这篇文章将介绍通过 React.context 来管理流程图数据的实际应用

 

 

项目结构:

.
├── Graph
│   └── index.jsx
├── Sider
│   └── index.jsx
├── Toolbar
│   └── index.jsx
├── components
│   ├── Edge
│   │   ├── LinkEdge.jsx
│   │   └── PopoverCard.jsx
│   ├── Modal
│   │   ├── RelationNodeForm.jsx
│   │   └── index.jsx
│   └── Node
│       └── RelationNode.jsx
├── context
│   ├── actions.js
│   ├── index.js
│   └── reducer.js
├── flow.css
└── flow.jsx

结合项目代码食用更香,仓库地址:https://github.com/wisewrong/bolg-demo-app/tree/main/flow-demo-app

 

 

一、定义 state

代码未敲,设计先行。在正式动工之前,先想清楚应该维护哪些数据

首先是 React Flow 的画布实例 reactFlowInstance,它会在 Graph.jsx 中创建并使用

另外 Toolbar.jsx 中保存的时候也会用到 reactFlowInstance,所以可以将它放到 context 中维护


然后是 React Flow 的节点/连线信息 elements,以及每个节点/连线对应的配置信息,它们可以放到 elements 中,通过每个元素的 data 来维护

但我更倾向于将业务数据拆开,用 elements 维护坐标等画布信息,另外创建一个 Map 对象 flowData 来维护业务数据


配置节点/连线业务数据的表单通常是放在 Modal 或 Drawer 里,它们肯定会放到画布外 难道还能放到节点里?,但通过节点/连线来触发

所以还需要另外维护一个 modalConfig,来控制 Modal 的显示/隐藏,以及传入 Modal 的节点数据


所以最终的 state 是这样的:

const initState = { // 画布实例
  reactFlowInstance: null, // 节点数据、连线数据
 elements: [], // 画布数据
  flowData: new Map(), // 弹窗信息
 modalConfig: { visible: false, nodeType: '', nodeId: '', }, };

 

 

二、创建 context

管理整个画布的状态,自然就会用到 useReducer

为了便于维护,我将整个 context 拆为三部分:index.js、reducer.js、actions.js

其中 actions.js 用来管理 dispatch 的事件名称:

// context/actions.js
 export const SET_INSTANCE = 'set_instance'; export const SET_ELEMENTS = 'set_elements'; export const SET_FLOW_NODE = 'set_flow_node'; export const REMOVE_FLOW_NODE = 'remove_flow_node'; export const OPEN_MODAL = 'open_modal'; export const CLOSE_MODAL = 'close_modal';

reducer.js 管理具体的事件处理逻辑

// context/reducer.js
 import * as Actions from "./actions"; // 保存画布实例
const setInstance = (state, reactFlowInstance) => ({ ...state, reactFlowInstance, }); // 设置节点/连线数据
const setElements = (state, elements) => ({ ...state, elements: Array.isArray(elements) ? elements : [], }); // 保存节点配置信息
const setFlowNode = (state, node) => { // ...
}; // 删除节点,同时删除节点配置信息
const removeFlowNode = (state, node) => { // ...
}; const openModal = (state, node) => { // ...
} const closeModal = (state) => { // ...
} // 管理所有处理函数
const handlerMap = { [Actions.SET_INSTANCE]: setInstance, [Actions.SET_FLOW_NODE]: setFlowNode, [Actions.REMOVE_FLOW_NODE]: removeFlowNode, [Actions.OPEN_MODAL]: openModal, [Actions.CLOSE_MODAL]: closeModal, [Actions.SET_ELEMENTS]: setElements, }; const reducer = (state, action) => { const { type, payload } = action; const handler = handlerMap[type]; const res = typeof handler === "function" && handler(state, payload); return res || state; }; export default reducer;

最后 index.js 管理初始状态,并导出相关产物

// context/index.js
 import React, { createContext, useReducer } from 'react'; import reducer from './reducer'; import * as Actions from './actions'; const FlowContext = createContext(); const initState = { // 画布实例
  reactFlowInstance: null, // 节点数据、连线数据
 elements: [], // 画布数据
  flowData: new Map(), // 弹窗信息
 modalConfig: { visible: false, nodeType: '', nodeId: '', }, }; const FlowContextProvider = (props) => { const { children } = props; const [state, dispatch] = useReducer(reducer, initState); return ( <FlowContext.Provider value={{ state, dispatch }}> {children} </FlowContext.Provider> ); }; export { FlowContext, FlowContextProvider, Actions };

 

 

三、节点的添加与删除

建立好状态管理体系之后,就可以通过 Provider 使用了

// flow.jsx
 import React from 'react'; import { ReactFlowProvider } from 'react-flow-renderer'; import Sider from './Sider'; import Graph from './Graph'; import Toolbar from './Toolbar'; import Modal from './components/Modal'; // 引入 Provider
import { FlowContextProvider } from './context'; import './flow.css'; export default function FlowPage() { return ( <div className="container">
      <FlowContextProvider>
        <ReactFlowProvider> {/* 顶部工具栏 */} <Toolbar />
          <div className="main"> {/* 侧边栏,展示可拖拽的节点 */} <Sider /> {/* 画布,处理核心逻辑 */} <Graph />
          </div> {/* 弹窗,配置节点数据 */} <Modal />
        </ReactFlowProvider>
      </FlowContextProvider>
    </div> ); }

 

上一篇文章《React Flow 实战(二)—— 拖拽添加节点》已经介绍过拖放节点,这里就不再赘述拖拽的实现

在添加节点之后,需要通过 reducer 中的方法来更新数据

// Graph/index.jsx
 import React, { useRef, useContext } from "react"; import ReactFlow, { addEdge, Controls } from "react-flow-renderer"; import { FlowContext, Actions } from "../context"; export default function FlowGraph(props) { const { state, dispatch } = useContext(FlowContext); const { elements, reactFlowInstance } = state; const setReactFlowInstance = (instance) => { dispatch({ type: Actions.SET_INSTANCE, payload: instance, }); }; const setElements = (els) => { dispatch({ type: Actions.SET_ELEMENTS, payload: els, }); }; // 画布加载完毕,保存当前画布实例
  const onLoad = (instance) => setReactFlowInstance(instance); // 连线
  const onConnect = (params) => setElements( addEdge( { ...params, type: "link", }, elements ) ); // 拖拽完成后放置节点
  const onDrop = (event) => { event.preventDefault(); const newNode = { // ...
 }; dispatch({ type: Actions.SET_FLOW_NODE, payload: { id: newNode.id, ...newNode.data, }, }); setElements(elements.concat(newNode)); }; // ...
}

同时在 reducer.js 中完善相应的逻辑,通过节点 id 维护节点数据

// context/reducer.js // 保存节点配置信息
const setFlowNode = (state, node) => { const nodeId = node?.id; if (!nodeId) return state; state.flowData.set(nodeId, node); return state; }; // ...

由于 elements 和 flowData 已经解耦,所以如需更新节点数据,直接使用 setFlowNode 更新 flowData 即可,不需要操作 elements

而如果是删除节点,可以通过 ReactFlow 提供的 removeElements 方法来快速处理 elements

// context/reducer.js
 import { removeElements } from "react-flow-renderer"; // 删除节点,同时删除节点配置信息
const removeFlowNode = (state, node) => { const { id } = node; const { flowData } = state; const res = { ...state }; if (flowData.get(id)) { flowData.delete(id); res.elements = removeElements([node], state.elements); } return res; }; // ...

节点数据的增删改就完成了,只要保证在所有需要展示节点信息的地方(画布节点、弹窗表单、连线弹窗)都通过 flowData 获取,维护起来就会很轻松

 

 

四、弹窗表单

最后再聊一聊关于弹窗表单的设计

一开始设计 state 的时候就提到过,整个画布只有一个弹窗,为此还专门维护了一份 modalConfig

弹窗可以只有一个,但不同类型的节点对应的表单却各有不同,这时候就需要创建不同的表单组件,通过节点类型来切换

// Modal/index.jsx
 import React, { useContext, useRef } from "react"; import { Modal } from "antd"; import RelationNodeForm from "./RelationNodeForm"; import { FlowContext, Actions } from "../../context"; // 通过节点类型来切换对应的表单组件
const componentsMap = { relation: RelationNodeForm, }; export default function FlowModal() { const formRef = useRef(); const { state, dispatch } = useContext(FlowContext); const { modalConfig } = state; const handleOk = () => { // 组件内部需要暴露一个 submit 方法
    formRef.current.submit().then(() => { dispatch({ type: Actions.CLOSE_MODAL }); }); }; const handleCancel = () => dispatch({ type: Actions.CLOSE_MODAL }); const Component = componentsMap[modalConfig.nodeType]; return ( <Modal title="编辑节点" visible={modalConfig.visible} onOk={handleOk} onCancel={handleCancel}> {Component && <Component ref={formRef} />} </Modal> ); }

但不同的表单组件,最后都是通过弹窗 footer 上的“确定”按钮来提交,而提交表单的逻辑却有可能不同

我这里的做法是,在表单组件内部暴露一个 submit 方法,通过弹窗的 onOk 回调触发

// Modal/RelationNodeForm.jsx
 import React, { useContext, useEffect, useImperativeHandle } from "react"; import { Input, Form } from "antd"; import { FlowContext, Actions } from "../../context"; function RelationNodeForm(props, ref) { const { state, dispatch } = useContext(FlowContext); const { flowData, modalConfig } = state; const [form] = Form.useForm(); const initialValues = flowData.get(modalConfig.nodeId) || {}; useImperativeHandle(ref, () => ({ // 将 submit 方法暴露给父组件
    submit: () => { return form .validateFields() .then((values) => { dispatch({ type: Actions.SET_FLOW_NODE, payload: { id: modalConfig.nodeId, ...values, }, }); }) .catch((err) => { return false; }); }, })); useEffect(() => { form.resetFields(); }, [modalConfig.nodeId, form]); return ( <Form form={form} initialValues={initialValues}> {/* Form.Item */} </Form> ); } export default React.forwardRef(RelationNodeForm);

 


关于 React Flow 的实战就到这里了,本文介绍的是状态管理,所以很多业务代码就没有贴出来

有需要的可以看下 GitHub 上的代码,仓库地址在本文的开头已经贴出来了

总的来说 React Flow 用起来还是挺方便,配合良好的状态管理体系,应该能适用于大部分的流程图需求

如果以后遇到了相当复杂的场景,我会再分享出来~

 


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM