Xây dựng biểu đồ quy trình với React Flow

1. Giới thiệu về React Flow React Flow là một thư viện mạnh mẽ để xây dựng các ứng dụng dựa trên nút (node). Bạn có thể sử dụng nó để tạo các biểu đồ tĩnh đơn giản hoặc các trình soạn thảo nút phức tạp. Thư viện này hỗ trợ tùy chỉnh loại nút và loại cạnh, đi kèm với các thành phần hữu ích như Mini Map (bản đồ thu nhỏ) và Controls (bộ điều khiển).

2. Cài đặt React Flow

  npm install react-flow-renderer    # npm
  yarn add react-flow-renderer       # Yarn
3. Sử dụng cơ bản của React Flow

1. Tạo nút với định dạng cố định và nội dung tùy chỉnh

Mã nguồn

index.tsx

import React from 'react';
import ReactFlow, {
  addEdge,
  MiniMap,
  Controls,
  Background,
  useNodesState,
  useEdgesState,
} from 'react-flow-renderer';

import { initialNodeData, initialConnectionData } from './initial-elements';

const FlowDiagram = () => {
  const [nodeItems, setNodeItems, onNodeItemsChange] = useNodesState(initialNodeData);
  const [connectionLines, setConnectionLines, onConnectionLinesChange] = useEdgesState(initialConnectionData);
  const handleConnection = (connectionParams) => setConnectionLines((existingLines) => addEdge(connectionParams, existingLines));

  return (
    <ReactFlow
      nodes={nodeItems} // Danh sách các nút
      edges={connectionLines} // Danh sách các kết nối
      onNodesChange={onNodeItemsChange} // Xử lý thay đổi nút (kéo, thay đổi kích thước...)
      onEdgesChange={onConnectionLinesChange} // Xử lý thay đổi kết nối
      onConnect={handleConnection} // Xử lý kết nối giữa các nút
      fitView // Tự động điều chỉnh để hiển thị tất cả nút
      attributionPosition="top-right" // Vị trí watermark của react-flow
    >
      // Nền có thể tùy chỉnh màu sắc và kích thước ô lưới
      <Background color="#aaa" gap={16} />
    </ReactFlow>
  );
};

export default FlowDiagram;

initial-elements.ts - Dữ liệu nút và kết nối

import { MarkerType } from 'react-flow-renderer';

export const initialNodeData = [
  {
    id: '1', // ID bắt buộc
    type: 'input', // Loại: input (bắt đầu), default (mặc định), output (kết thúc)
    data: { // Dữ liệu bổ sung
      label: ( // Nhãn nút
        <>
          Chào mừng đến với <strong>React Flow!</strong>
        </>
      ),
    },
    position: { x: 250, y: 0 }, // Vị trí nút
  },
  {
    id: '2',
    data: {
      label: (
        <>
          Đây là một <strong>nút mặc định</strong>
        </>
      ),
    },
    position: { x: 100, y: 100 },
  },
  {
    id: '3',
    data: {
      label: (
        <>
          Nút này có <strong>kiểu tùy chỉnh</strong>
        </>
      ),
    },
    position: { x: 400, y: 100 },
    style: {
      background: '#D6D5E6',
      color: '#333',
      border: '1px solid #222138',
      width: 180,
    },
  },
  {
    id: '4',
    position: { x: 250, y: 200 },
    data: {
      label: 'Một nút mặc định khác',
    },
  },
  {
    id: '5',
    data: {
      label: 'ID nút: 5',
    },
    position: { x: 250, y: 325 },
  },
  {
    id: '6',
    type: 'output',
    data: {
      label: (
        <>
          Một <strong>nút kết thúc</strong>
        </>
      ),
    },
    position: { x: 100, y: 480 },
  },
  {
    id: '7',
    type: 'output',
    data: { label: 'Một nút kết thúc khác' },
    position: { x: 400, y: 450 },
  },
];

export const initialConnectionData = [
  { id: 'e1-2', source: '1', target: '2', label: 'đây là nhãn của kết nối' },
  { id: 'e1-3', source: '1', target: '3' },
  {
    id: 'e3-4', // ID bắt buộc
    source: '3', // ID nút nguồn
    target: '4', // ID nút đích
    animated: true, // Hiệu ứng động
    label: 'kết nối có hiệu ứng', // Nhãn kết nối
  },
  {
    id: 'e4-5',
    source: '4',
    target: '5',
    label: 'kết nối với mũi tên',
    markerEnd: { // Mũi tên ở cuối
      type: MarkerType.ArrowClosed,
    },
  },
  {
    id: 'e5-6',
    source: '5',
    target: '6',
    type: 'smoothstep', // Loại kết nối: default, straight, step, smoothstep
    label: 'kết nối mềm mại',
  },
  {
    id: 'e5-7',
    source: '5',
    target: '7',
    type: 'step',
    style: { stroke: '#f6ab6c' }, // Màu kết nối
    label: 'kết nối dạng bước',
    animated: true,
    labelStyle: { fill: '#f6ab6c', fontWeight: 700 }, // Kiể chữ nhãn
  },
];

Kết quả hiển thị

2. Tùy chỉnh nội dung và kiểu dáng cho từng nút

Đây là phiên bản tĩnh của biểu đồ quy trình. Để có thể kéo thả các nút, hãy thêm các tham số như trong phần 1 (onNodesChange...).

index.tsx

import React, {useEffect} from 'react';

import ReactFlow, {
    useNodesState,
    useEdgesState,
} from 'react-flow-renderer';

import { initialNodeData, initialConnectionData } from './initial-elements';
import CustomNodeComponent from './CustomNodeRenderer'; // Thành phần tùy chỉnh để hiển thị nút

const nodeTypeMap = {
    custom: CustomNodeComponent, // Thành phần tùy chỉnh
};
const FlowDiagram = ({resizeTrigger}: any) => {
    const [nodeItems, setNodeItems] = useNodesState(initialNodeData);
    const [connectionLines] = useEdgesState(initialConnectionData);

    useEffect(() => {
        setNodeItems([]);
        setTimeout(() => {
            setNodeItems(initialNodeData);
        }, 50);
    }, [resizeTrigger]);

    if (!nodeItems?.length) {
        return null;
    }

    return (
        <ReactFlow
            nodes={nodeItems} // Danh sách các nút
            edges={connectionLines} // Danh sách các kết nối
            panOnDrag={false}
            zoomOnDoubleClick={false}
            zoomOnPinch={false}
            zoomOnScroll={false}
            panOnScroll={false}
            fitView // Tự động điều chỉnh để hiển thị tất cả nút
            nodeTypes={nodeTypeMap}
            attributionPosition="top-left" // Vị trí watermark của react-flow
        >
            {/* <Background color="#aaa" gap={16} /> */}
        </ReactFlow>
    );
};

export default FlowDiagram;

initial-elements.ts

import {MarkerType, Position} from 'react-flow-renderer';

const baseStyle = {
    color: '#333',
    border: '1px solid #4E8FF0',
    borderRadius: '5px',
    background: 'white',
};

export const initialNodeData = [
    {
        id: '0', 
        type: 'custom',// Có input, output, default - input chỉ có đầu ra, output chỉ có đầu vào, default có cả hai hoặc tùy chỉnh
        data: {
            label: '',
        },
        position: {x: -20, y: 40}, // Vị trí nút
        style: {
            width: 1550,
            height: 500,
            border: '1px solid #91caff',
            borderRadius: '15px',
            color: '#4585F2',
            background: '#E2E6F3',
            zIndex: -2,
        },
    },
    {
        id: '1', // ID bắt buộc
        type: 'custom', // Loại: input (bắt đầu), default (mặc định), output (kết thúc)
        data: { // Dữ liệu bổ sung
            label:
                'Nhiệm vụ 1',
        },
        position: {x: 200, y: 70}, // Vị trí nút
        style: {
            width: 200,
            height: 150,
            ...baseStyle,
        },
    },
    {
        id: '2',
        type: 'custom',
        data: {
            label: 'Nhiệm vụ 2',
        },
        position: {x: 450, y: 70},
        style: {
            width: 200,
            height: 150,
        },
    },
    {
        id: '3',
        type: 'custom',

        data: {
            label: (
                'Nhiệm vụ 3'
            ),
        },
        position: {x: 700, y: 70},
        style: {
            width: 200,
            height: 150,
            ...baseStyle,
        },
    }
];

export const initialConnectionData = [
    {
        id: '1-2',
        source: '1',
        target: '2',
        markerEnd: { // Mũi tên ở cuối
            type: MarkerType.ArrowClosed,
            color: '#4E8FF0',
        },
        style: {stroke: '#4E8FF0'}, // Màu kết nối
        labelStyle: {fill: '#4E8FF0', fontWeight: 700}, // Kiể chữ nhãn
    },
    {
        id: '2-3', // ID bắt buộc
        source: '2', // ID nút nguồn
        target: '3', // ID nút đích
        markerEnd: { // Mũi tên ở cuối
            type: MarkerType.ArrowClosed,
            color: '#4E8FF0',
        },
        style: {stroke: '#4E8FF0'}, // Màu kết nối
        labelStyle: {fill: '#4E8FF0', fontWeight: 700}, // Kiể chữ nhãn
    },
    {
        id: '3-4',
        source: '3',
        target: '4',
        style: {stroke: '#4E8FF0'}, // Màu kết nối
        labelStyle: {fill: '#4E8FF0', fontWeight: 700}, // Kiể chữ nhãn
        markerEnd: { // Mũi tên ở cuối
            type: MarkerType.ArrowClosed,
            color: '#4E8FF0',
        },
    }
];
CustomNodeRenderer.tsx
import React, {memo} from 'react';
import {Handle, Position} from 'react-flow-renderer';
import styleClasses from './styles.module.scss';
import newTaskIcon from '@/assets/new-task.png';
import operatorIcon from '@/assets/operator-config.png';
import deployIcon from '@/assets/deployment-task.png';
import reviewIcon from '@/assets/task-review.png';
import launchIcon from '@/assets/task-launch.png';
import {Button} from 'antd';

const nodeDataList = [
    {
        id: '0',
    },
    {
        src: newTaskIcon,
        id: '1',
        buttonText: 'Bắt đầu nhanh',
        url: '',
        width: '50px',
        height: '50px',
    },
    {
        src: operatorIcon,
        id: '11',
        buttonText: 'Hướng dẫn cấu hình',
        link: '',
        width: '45px',
        height: '45px',
    },
    {
        src: deployIcon,
        id: '3',
        title: 'Thông tin nhiệm vụ',
        width: '55px',
        height: '55px',
    },

];

export default memo(({nodeInfo, nodeId, canConnect}: any) => {

    const getPosition = (nodeId: any) => {
        switch (nodeId) {
            case '6':
                return Position.Right;
            case '9':
                return Position.Top;
            default:
                return Position.Left;
        }
    };

    const renderActionButton = (item: any) => {
        if (item.link) {
            return (
                <Button
                    target='_blank'
                    type='link'
                    htmlType='button'
                    href={item.link}
                > {item.buttonText}
                </Button>
            );
        } else {
            return (
                <Button
                    style={{
                        background: 'linear-gradient(90deg,#2468E8,#2C61E4,#4148D0,#5127B8)',
                        border: 'none',
                    }}
                    onClick={e => {
                        e.stopPropagation();
                        window.location.hash = item.url;
                    }}
                    type='primary'
                > {item.buttonText}
                </Button>
            );
        }
    };

    return (
        <div className={styleClasses.customNode}>
            <Handle
                style={{visibility: 'hidden'}}
                type="target"
                position={getPosition(nodeId)}
                isConnectable={canConnect}
            />
            {
                nodeDataList.filter((item: any) => {
                    return item.id === nodeId;
                }).map((item: any) => {
                    return (
                        +item.id < 12
                            ? <div
                                key={item.id}
                                className={styleClasses.nodeContent}
                                style={nodeInfo.style}
                            >
                                {item.src ? <img style={{width: item.width, height: item.height}} src={item.src} /> : null}
                                <div className={styleClasses.nodeRightSection}>
                                    <p className={styleClasses.nodeLabel}>{nodeInfo.label}</p>
                                    {item.buttonText
                                        ? renderActionButton(item)
                                        : <span className={styleClasses.nodeTitle}>{item.title}</span>}
                                </div>
                            </div>
                            : <div
                                key={item.id}
                                className={styleClasses.dataDistribution}
                                style={nodeInfo.style}
                            >
                                {
                                    item.order
                                        ? <p className={styleClasses.circle}>
                                            {item.order}
                                        </p> : null
                                }
                                <p className={styleClasses.nodeLabel}>{nodeInfo.label}</p>
                                {
                                    item.buttonText ? renderActionButton(item) : null
                                }
                            </div>
                    );
                })
            }
            <Handle
                style={{visibility: 'hidden'}}
                type='source'
                position={nodeId === '11'
                || nodeId === '5' ? Position.Bottom : Position.Right}
                id='a'
                className='my_handle'
                isConnectable={canConnect}
            />
        </div>
    );
});

Kết quả hiển thị

Một số tài liệu tham khảo về tham số của react-flow và địa chỉ trang chủ:

https://www.5axxw.com/wiki/content/obkffc

https://reactflow.dev/

Thẻ: react-flow flowchart node-based-editor JavaScript react

Đăng vào ngày 13 tháng 6 lúc 19:06