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~