使用 React Flow 打造專業組織架構圖的快速指南

使用 React Flow 打造專業組織架構圖的快速指南

在開發企業內部系統或資訊平台的專案中,常常會遇到需要呈現樹狀結構與流程順序圖示的情境,例如公司組織架構圖、部門關係圖、工作流程圖等。過去多數解法是透過繪圖工具產出靜態圖片,但這類圖示無法互動、無法拖曳,也難以維護或動態更新。

即使採用部分 UI 套件或視覺工具來產生流程圖或組織圖,常常也需要手動調整方塊位置、對齊箭頭,更新資料時流程容易走樣,增加維護負擔。

本文將示範如何使用 React Flow,打造一套互動式、資料驅動的組織架構圖,以「組織 → 部門 → 子部門」的層級為範例,說明如何:

  • 自動渲染節點與連線箭頭
  • 自訂每個節點的樣式與內容
  • 利用資料結構控制整體圖示架構與層級
  • 一鍵更新資料即反映圖面變化

透過 React Flow,不只可以製作清晰的組織結構圖,也非常適合建構如流程圖、決策圖、網路架構圖、節點流程設計工具等需要「前後關係」與「階層視覺化」的應用場景。

整體來說,這樣的開發方式讓你:

  • 更快速建立組織圖與流程圖
  • 減少手動排版時間
  • 確保圖示維持一致與整齊
  • 提高可讀性與可維護性

如果你正在找尋一個現代化、可維護、互動性高的組織圖解決方案,React Flow 將是非常值得一試的選項。

一、基本介紹

React Flow 是一個開源的 React 函式庫,用於在 Web 應用程式中建立互動式、基於節點的圖表和流程圖,支援自訂節點和邊線樣式、縮圖顯示和控制元件等功能,廣泛應用於資料視覺化、工作流程編輯器、機器學習等領域。 

  • 安裝 React-flow:npm install @xyflow/react
  • 基本元件認識:
    • Node(節點):負責顯示資料的小格子
    • Handle(把手):負責連結節點與節點的原點,分成source跟target兩種,source代表連結從這裡出發,target則代表連結目的地
    • Edge(連接線):source跟target間的連接線
react-flow基本元件
react-flow基本元件

接下來就開始處理資料的顯示,也可以使用react-flow預設的節點樣式,但T編想自訂自己的樣式所以要先新增一個自訂節點。

二、Custom Nodes(自訂節點):

先新增一個react component,舉例想新增一個組織的節點:

import React from 'react';
import { Paper, Typography } from '@mui/material';
import type { Organization } from '../types';
import { Handle } from '@xyflow/react';

interface OrganizationNodeProps {
    data: Organization;
  }

export function OrganizationNode({data}: OrganizationNodeProps) {
  return (
    <>
    <Paper variant="outlined" sx={{
        padding: '12px',
        border:'2px solid rgba(25, 118, 210)',
        minWidth:'100px',
        display:'flex',
        alignItems:'center',
        justifyContent:'center',
      }}>
        <Typography>{data.org_name}</Typography>
      </Paper>
      <Handle type="source" position='bottom'/>
    </>
      
  );
}

T編想顯示組織名稱以及有顏色的框線,因此先接收從外部傳進來的data(組織資料),並在內部顯示data.org_name。Handle在這裡就可以加進來了,組織為第一層,因此在組織節點的下面加一個source handle。

部門節點也類似只是要多一個<Handle type="target" position="top" /> 因為他要用接收來自組織節點的連結。

三、主畫面

接下來就是將我們剛剛新增的自訂節點渲染至主畫面了

import '@xyflow/react/dist/style.css';//記得載入官方CSS樣式

export default function App() {
  return (
    <div style={{ height: '100%', width: '100%' }}>
      <ReactFlow>
        <Background />
        <Controls />
      </ReactFlow>
    </div>
  );
}

在官方文件裡面有說明外面一定要包一個父元件定好寬度及高度。

這樣就先創好了一個空的流程畫布 長這樣 :

這裡有幾個reactflow的內建元件<Background /> 為後面的點點背景、<Controls /> 為左邊的控制按鈕。

接下來將節點渲染上去:

先宣告自訂節點類型

import { OrganizationNode } from "../../components/OrganizationNode";
import { GroupNode } from "../../components/GroupNode";

const nodeTypes: NodeTypes = {
  organizationNode: OrganizationNode,
  groupNode: GroupNode,
};

接著將節點類型傳給reactflow:

<ReactFlow
  nodes={nodes}
  edges={edges}
  nodeTypes={nodeTypes} //<-這行
  edgeTypes={edgeTypes}
  fitView
  nodesConnectable={false}>
 </ReactFlow>

接著就可以宣告節點了,官方文件的宣告節點方法:

const nodes = [
  {
    id: 'node-1',
    //你剛剛新增的節點類型 像我就會是organizationNode或groupNode
    type: 'textUpdater',
    position: { x: 0, y: 0 },//節點初始化的位置
    data: { value: 123 },//資料
  },
];

而T編的需求是需要節點根據api傳回來的資料渲染,假如組織的資料傳回來是這樣:

{
    id: 1,
    org_name: "XX組織";
  }

接著就新增組織節點

//用來拿組織資料的hook
const { data: orgData, loading, error } = useOneOrganizationData(id);

const orgNode: Node<Organization> = {
        id: `org-${orgData.id}`,
        type: "organizationNode",
        data: orgData,
        position: { x: 200, y: 0 },
      };

因為傳進去reactflow的nodes一定是要一個array

<ReactFlow
	nodes={nodes} //<-這行
  edges={edges}
  nodeTypes={nodeTypes}
  edgeTypes={edgeTypes}
  fitView
  nodesConnectable={false}>
 </ReactFlow>

因此先宣告一個nodes陣列,接著將orgNode push進去,完整程式碼:

const nodes: Node[] = [];

    if (orgData) {//拿到資料後再新增
      const orgNode: Node<Organization> = {
        id: `org-${orgData.id}`,
        type: "organizationNode",
        data: orgData,
        position: { x: 200, y: 0 },
      };
      nodes.push(orgNode);
    }

nodes就會長這樣:

[
{id: "org-23", type: "organizationNode", data: Object, position: {x: 200, y: 0}},
]

接著組織節點就渲染出來了

這裡用 push 是最簡單的示範,實務上你可能會用 useState 或 useNodesState 來管理節點狀態,才能支援互動(拖曳、動態新增節點)。

const [nodes, setNodes, onNodesChange] = useNodesState([]);

const [edges, setEdges, onEdgesChange] = useEdgesState([]);

組織下的部門節點也是同理,只是在專案中組織可能會有複數個,因此要用array先將部門節點map出來,再加入nodes陣列裡:

const groups: Group[] = Array.isArray(groupData) ? (groupData as Group[]) : [];
    groups.forEach((g, i) => {
      const gNode: Node<Group> = {
        id: `g-${g.id}`,
        type: "groupNode",
        data: g,
        position: { x: 200+i*200, y:100 },
      };
      nodes.push(gNode);
    });

接下來就是處理組織與部門間的連線(Edges)了

四、Custom Edges(自訂邊)

跟自訂節點類似,也可以去自訂邊的樣式,根據官方文件先新增一個自訂節點元件

import { BaseEdge, getStraightPath } from '@xyflow/react';
 
export function CustomEdge({ id, sourceX, sourceY, targetX, targetY }) {
  const [edgePath] = getStraightPath({//根據傳進來的xy計算直線路徑
    sourceX,
    sourceY,
    targetX,
    targetY,
  });
 
  return (
    <>
      <BaseEdge id={id} path={edgePath} />
    </>
  );
}

reactflow也提供了很多方便計算路徑的函數,比如:

  • getBezierPath
  • getSmoothStepPath
  • getStraightPath

只是不同函數需要的props也會有所不同,這次先以最簡單的直線為例,接著跟節點一樣建立edgeTypes

const edgeTypes = {
  'custom-edge': CustomEdge,
};

然後將edgeType傳遞給ReactFlow

<ReactFlow
	nodes={nodes}
  edges={edges}
  nodeTypes={nodeTypes} 
  edgeTypes={edgeTypes} //<-這行
  fitView
  nodesConnectable={false}>
 </ReactFlow>

接著建立一個初始連線,官方的文件的範例:

const initialEdges = [
  {
    id: 'e1',
    source: 'n1', //開始節點id
    target: 'n2', //目標節點id
    type: 'custom-edge',//記得將type改成剛自訂好的edgeTypes
  },
];

節點間就建立連線了!

接著到我們這次的專案裡,我們的需求是將組織與底下的所有部門建立連線,因為線會不只一條,因此我們一樣先存進array裡再map出來

if (!orgData) return [];
    const groups: Group[] = Array.isArray(groupData) ? (groupData as Group[]) : [];
    return groups.map((g) => ({
      id: `e-${orgData.id}-${g.id}`,//e-組織id-部門id
      source: `org-${orgData.id}`,//都是從最上層的組織出發
      target: `g-${g.id}`,//底下的部門id
      type: "custom-edge",
      animated: true,//將連線添加動畫效果,有方向性比較了解上下關係
    }));
  }, [orgData, groupData]);

實際渲染時請以useMemo穩定nodeTypes/edgeTypes的參考,並以useNodesState / useEdgesState受控管理nodes/edges;若由資料導出nodes/edges,至少用useMemo在資料不變時保持參考穩定,以避免節點被卸載重載(例如拖曳位置重置)。

五、最後成果:

這個library還有非常多非常多功能,比如拖拉節點、白板功能、排列演算法,也可以去官方的playground玩玩看:https://play.reactflow.dev

Loading

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *