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 用起來還是挺方便,配合良好的狀態管理體系,應該能適用於大部分的流程圖需求
如果以後遇到了相當複雜的場景,我會再分享出來~