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ủ: