學習進階使用

計算流程

在本指南中,我們假設您已了解 React Flow 的核心概念,以及如何實作自訂節點

通常使用 React Flow 時,開發人員會在 React Flow 之外處理資料,將其傳送到其他地方,例如伺服器或資料庫。相反地,在本指南中,我們將展示如何在 React Flow 內部直接計算資料流程。您可以使用它來根據連線資料更新節點,或建構完全在瀏覽器內部執行的應用程式。

我們將建構什麼?

在本指南結束時,您將建構一個互動式流程圖,從三個獨立的數字輸入欄位(紅色、綠色和藍色)產生顏色,並判斷在該背景顏色上,白色或黑色文字會更易讀。

export default function App() {
  const data: string = "world"

  return <h1>Hello {data}</h1>
}

唯讀

建立自訂節點

我們先建立一個自訂輸入節點(NumberInput.js),並新增三個它的實例。我們將使用受控的 <input type="number" />,並在 onChange 事件處理函式內將其限制為 0 - 255 之間的整數。

import { useCallback, useState } from 'react';
import { Handle, Position } from '@xyflow/react';
 
function NumberInput({ id, data }) {
  const [number, setNumber] = useState(0);
 
  const onChange = useCallback((evt) => {
    const cappedNumber = Math.round(
      Math.min(255, Math.max(0, evt.target.value)),
    );
    setNumber(cappedNumber);
  }, []);
 
  return (
    <div className="number-input">
      <div>{data.label}</div>
      <input
        id={`number-${id}`}
        name="number"
        type="number"
        min="0"
        max="255"
        onChange={onChange}
        className="nodrag"
        value={number}
      />
      <Handle type="source" position={Position.Right} />
    </div>
  );
}
 
export default NumberInput;

接下來,我們將新增一個新的自訂節點(ColorPreview.js),每個顏色通道都有一個目標控制點,並具有顯示結果顏色的背景。我們可以利用 mix-blend-mode: 'difference'; 來使文字顏色始終可讀。

當您在單一節點上有相同種類的多個控制點時,別忘了為每個控制點提供一個單獨的 id!

在處理時,我們也將邊緣從輸入節點新增到顏色節點到我們的 initialEdges 陣列。

export default function App() {
  const data: string = "world"

  return <h1>Hello {data}</h1>
}

唯讀

計算資料

我們如何從輸入節點取得資料到顏色節點?這是一個兩步驟的過程,涉及為此特定目的建立的兩個 Hook

  1. 使用updateNodeData 回呼,將每個數字輸入值儲存在節點的 data 物件內。
  2. 使用useHandleConnections找出哪些節點已連線,然後使用useNodesData接收來自連線節點的資料。

步驟 1:將數值寫入資料物件

首先,讓我們在 initialNodes 陣列中的 data 物件內為輸入節點新增一些初始值,並將它們用作輸入節點的初始狀態。然後,我們將從useReactFlow Hook 中取得函式updateNodeData,並在輸入變更時使用它來更新節點的 data 物件,並提供新值。

預設情況下,您傳遞給updateNodeData的資料將與舊的資料物件合併。這使得部分更新更容易,並在您忘記新增 {...data} 時為您提供保護。您可以傳遞 { replace: true } 作為選項來取代物件。

export default function App() {
  const data: string = "world"

  return <h1>Hello {data}</h1>
}

唯讀
⚠️

在處理輸入欄位時,您不希望直接使用節點的 data 物件作為 UI 狀態。

更新資料物件會有延遲,而且游標可能會不穩定地跳動並導致不必要的輸入。

步驟 2:從連線的節點取得資料

首先,使用useHandleConnections Hook 判斷每個控制點的所有連線,然後使用updateNodeData擷取第一個連線節點的資料。

請注意,每個控制點可以連線多個節點,您可能希望在應用程式中將連線數量限制為單一控制點。查看連線限制範例,以了解如何執行此操作。

完成! 嘗試變更輸入值,並即時查看顏色變化。

export default function App() {
  const data: string = "world"

  return <h1>Hello {data}</h1>
}

唯讀

改進程式碼

首先取得連線,然後為每個控制點單獨取得資料似乎很彆扭。對於具有多個控制點的節點(例如這些),您應該考慮建立一個自訂的控制點組件,以隔離連線狀態和節點資料繫結。我們可以建立一個內聯。

ColorPreview.js
// {...}
function CustomHandle({ id, label, onChange }) {
  const connections = useHandleConnections({
    type: 'target',
    id,
  });
 
  const nodeData = useNodesData(connections?.[0].source);
 
  useEffect(() => {
    onChange(nodeData?.data ? nodeData.data.value : 0);
  }, [nodeData]);
 
  return (
    <div>
      <Handle
        type="target"
        position={Position.Left}
        id={id}
        className="handle"
      />
      <label htmlFor="red" className="label">
        {label}
      </label>
    </div>
  );
}

我們可以將顏色提升到本機狀態,並宣告每個控制點,如下所示

ColorPreview.js
// {...}
function ColorPreview() {
  const [color, setColor] = useState({ r: 0, g: 0, b: 0 });
 
  return (
    <div
      className="node"
      style={{
        background: `rgb(${color.r}, ${color.g}, ${color.b})`,
      }}
    >
      <CustomHandle
        id="red"
        label="R"
        onChange={(value) => setColor((c) => ({ ...c, r: value }))}
      />
      <CustomHandle
        id="green"
        label="G"
        onChange={(value) => setColor((c) => ({ ...c, g: value }))}
      />
      <CustomHandle
        id="blue"
        label="B"
        onChange={(value) => setColor((c) => ({ ...c, b: value }))}
      />
    </div>
  );
}
 
export default ColorPreview;

變得更複雜

現在我們有一個簡單的範例,說明如何透過 React Flow 傳輸資料。如果我們想做更複雜的事情,例如沿途轉換資料呢?甚至是採用不同的路徑?我們也可以做到!

繼續流程

讓我們擴展我們的流程。首先,將一個輸出<Handle type="source" position={Position.Right} />新增到顏色節點,並移除本機組件狀態。

因為這個節點上沒有輸入欄位,我們根本不需要保留本地狀態。我們可以只讀取並直接更新節點的 data 物件。

接下來,我們新增一個新的節點(Lightness.js),它會接收一個顏色物件,並判斷該顏色是亮色還是暗色。我們可以利用相對亮度公式 luminance = 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b 來計算顏色的感知亮度(0 代表最暗,255 代表最亮)。我們可以假設任何 >= 128 的值都是亮色。

export default function App() {
  const data: string = "world"

  return <h1>Hello {data}</h1>
}

唯讀

條件分支

如果我們想根據感知亮度在流程中採取不同的路徑,該怎麼辦?讓我們為亮度節點提供兩個來源控制點 lightdark,並依來源控制點 ID 分隔節點的 data 物件。如果您有多個來源控制點,則需要這樣做才能區分每個來源控制點的資料。

但「採取不同路徑」是什麼意思?一種解決方案是假設連結到目標控制點的 nullundefined 資料被視為「停止」。在我們的案例中,如果傳入的顏色是亮色,我們可以將其寫入 data.values.light,如果傳入的顏色是暗色,則寫入 data.values.dark,並將另一個值設定為 null

別忘了加入 flex-direction: column;align-items: end; 來重新定位控制點標籤。

export default function App() {
  const data: string = "world"

  return <h1>Hello {data}</h1>
}

唯讀

酷!現在我們只需要最後一個節點來看看它是否真的有效... 我們可以建立一個自訂的除錯節點(Log.js)來顯示連結的資料,就完成了!

export default function App() {
  const data: string = "world"

  return <h1>Hello {data}</h1>
}

唯讀

總結

您已經學會如何在流程中移動資料並沿途轉換它。您只需要做到以下幾點:

  1. 利用 updateNodeData 回呼函數將資料儲存在節點的 data 物件中。
  2. 使用 useHandleConnections 找出哪些節點已連線,然後使用 useNodesData 來接收來自已連線節點的資料。

您可以透過將未定義的傳入資料視為「停止」來實作分支。順帶一提,大多數也具有分支的流程圖通常會將節點的觸發與連結到節點的實際資料分開。Unreal Engine 的藍圖就是一個很好的例子。

在您離開之前,最後一點:您應該找到一種一致的方式來建構所有節點資料,而不是像我們剛才那樣混合不同的想法。例如,這表示如果您開始使用按控制點 ID 分割資料的方式,您應該對所有節點都這樣做,無論它們是否有多個控制點。能夠對整個流程中資料的結構做出假設會讓您的生活輕鬆許多。