React Flow actual combat - use React.context to manage flow chart data

The previous two articles on React Flow have introduced how to draw a flowchart

In the actual project, each node or even each connection on the flowchart needs to maintain an independent business data

This article will introduce the practical application of managing flowchart data through React.context

 

 

Project structure:

.
├── 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

Combined with the project code, it tastes better. Warehouse address: https://github.com/wisewrong/bolg-demo-app/tree/main/flow-demo-app

 

 

1, Define state

Code not knocked, design first. Before the formal commencement of construction, first figure out what data should be maintained

The first is the canvas instance of React Flow, which will be displayed in the   Create and use in Graph.jsx

In addition, it will also be used when saving in Toolbar.jsx   reactFlowInstance, so it can be maintained in the context

Then there are the node / connection information elements of React Flow and the corresponding configuration information of each node / connection. They can be put into elements and maintained through the data of each element

But I prefer to split the business data, maintain the coordinates and other canvas information with elements, and create a Map object flowData to maintain the business data

The form for configuring node / connection business data is usually placed in Modal or   In the Drawer, they will certainly be placed outside the canvas   Can it still be put in the node?, But it is triggered by nodes / connections

So another one needs to be maintained   modalConfig to control the display / hiding of Modal and the node data passed into Modal

So the final state is as follows:

const initState = {
  // Canvas instance
  reactFlowInstance: null,
  // Node data and connection data
  elements: [],
  // Canvas data
  flowData: new Map(),
  // Pop up information
  modalConfig: {
    visible: false,
    nodeType: '',
    nodeId: '',
  },
};

 

 

2, Create context

Manage the state of the whole canvas, which will be used naturally   useReducer

To facilitate maintenance, I split the whole context into three parts: index.js, reducer.js and actions.js

among   actions.js is used to manage   Event name of 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 manages specific event processing logic

// context/reducer.js

import * as Actions from "./actions";

// Save canvas instance
const setInstance = (state, reactFlowInstance) => ({
  ...state,
  reactFlowInstance,
});

// Set node/Connection data
const setElements = (state, elements) => ({
  ...state,
  elements: Array.isArray(elements) ? elements : [],
});

// Save node configuration information
const setFlowNode = (state, node) => {
// ...
};

// Delete the node and delete the node configuration information at the same time
const removeFlowNode = (state, node) => {
  // ...
};

const openModal = (state, node) => {
  // ...
}

const closeModal = (state) => {
  // ...
}

// Manage all processing functions
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;

Finally, index.js manages the initial state and exports related products

// context/index.js

import React, { createContext, useReducer } from 'react';
import reducer from './reducer';
import * as Actions from './actions';

const FlowContext = createContext();

const initState = {
  // Canvas instance
  reactFlowInstance: null,
  // Node data and connection data
  elements: [],
  // Canvas data
  flowData: new Map(),
  // Pop up information
  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 };

 

 

3, Adding and deleting nodes

After the status management system is established, it can be used through the 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';
// introduce Provider
import { FlowContextProvider } from './context';

import './flow.css';

export default function FlowPage() {
  return (
    <div className="container">
      <FlowContextProvider>
        <ReactFlowProvider>
          {/* Top Toolbar  */}
          <Toolbar />
          <div className="main">
            {/* The sidebar shows the nodes that can be dragged */}
            <Sider />
            {/* Canvas, processing core logic */}
            <Graph />
          </div>
          {/* Pop up window to configure node data */}
          <Modal />
        </ReactFlowProvider>
      </FlowContextProvider>
    </div>
  );
}

 

Last article React Flow practice (II) - drag and drop to add nodes The drag and drop node has been introduced, and the implementation of drag and drop will not be repeated here

After adding nodes, you need to update the data through the methods in 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,
    });
  };

  // After loading the canvas, save the current canvas instance
  const onLoad = (instance) => setReactFlowInstance(instance);

  // Connect
  const onConnect = (params) =>
    setElements(
      addEdge(
        {
          ...params,
          type: "link",
        },
        elements
      )
    );

  // Place node after dragging
  const onDrop = (event) => {
    event.preventDefault();

    const newNode = {
      // ...
    };
    dispatch({
      type: Actions.SET_FLOW_NODE,
      payload: {
        id: newNode.id,
        ...newNode.data,
      },
    });
    setElements(elements.concat(newNode));
  };

  // ...
}

At the same time, the corresponding logic is improved in reducer.js, and the node data is maintained through the node id

// context/reducer.js

// Save node configuration information
const setFlowNode = (state, node) => {
  const nodeId = node?.id;
  if (!nodeId) return state;
  state.flowData.set(nodeId, node);
  return state;
};

// ...

Because elements and flowData have been decoupled, if you need to update node data, you can directly use setFlowNode to update flowData without operating elements

If the node is deleted, it can be provided through ReactFlow   removeElements method to quickly process elements

// context/reducer.js

import { removeElements } from "react-flow-renderer";

// Delete the node and delete the node configuration information at the same time
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;
};

// ...

The addition, deletion and modification of node data are completed. As long as it is ensured that all places where node information needs to be displayed (canvas node, pop-up form and connection Pop-Up) are obtained through flowData, it will be easy to maintain

 

 

4, Pop up form

Finally, let's talk about the design of pop-up form

It was mentioned at the beginning of designing state that there is only one pop-up window on the whole canvas, and a modalConfig is specially maintained for this purpose

There can be only one pop-up window, but the forms corresponding to different types of nodes are different. At this time, you need to create different form components and switch through node types

// Modal/index.jsx

import React, { useContext, useRef } from "react";
import { Modal } from "antd";
import RelationNodeForm from "./RelationNodeForm";
import { FlowContext, Actions } from "../../context";

// Switch the corresponding form components by node type
const componentsMap = {
  relation: RelationNodeForm,
};

export default function FlowModal() {
  const formRef = useRef();
  const { state, dispatch } = useContext(FlowContext);
  const { modalConfig } = state;

  const handleOk = () => {
    // One needs to be exposed inside the component submit method
    formRef.current.submit().then(() => {
      dispatch({ type: Actions.CLOSE_MODAL });
    });
  };

  const handleCancel = () => dispatch({ type: Actions.CLOSE_MODAL });

  const Component = componentsMap[modalConfig.nodeType];

  return (
    <Modal title="Edit node" visible={modalConfig.visible} onOk={handleOk} onCancel={handleCancel}>
      {Component && <Component ref={formRef} />}
    </Modal>
  );
}

However, different form components are finally submitted through the "OK" button on the pop-up footer, but the logic of submitting forms may be different

My approach here is to expose a submit method inside the form component and trigger it through the onOk callback in the pop-up window

// 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, () => ({
    // take submit Method is exposed to the parent component
    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);

 

That's all for the actual combat of React Flow. This article introduces state management, so many business codes are not posted

If necessary, you can see the code on GitHub. The warehouse address has been posted at the beginning of this article

In general, React Flow is very convenient to use. With a good state management system, it should be applicable to most flow chart requirements

If you encounter quite complex scenes in the future, I will share them again~

 

Keywords: React

Added by visonardo on Tue, 07 Dec 2021 16:56:15 +0200