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 用起來還是挺方便,配合良好的狀態管理體系,應該能適用於大部分的流程圖需求

如果以後遇到了相當複雜的場景,我會再分享出來~

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章