学ぶ高度な使用

計算フロー

このガイドでは、React Flowのコアコンセプトと、カスタムノードの実装方法について、すでに理解していることを前提としています。

通常、React Flowでは、開発者はデータをサーバーやデータベースなど、別の場所に送信して、React Flowの外部でデータを処理します。代わりに、このガイドでは、React Flow内で直接データフローを計算する方法を示します。これは、接続されたデータに基づいてノードを更新したり、ブラウザー内で完全に実行されるアプリを構築したりするために使用できます。

何を作るか?

このガイドの終わりまでに、3つの別々の数値入力フィールド(赤、緑、青)から色を生成し、その背景色で白または黒のテキストのどちらが読みやすいかを判断するインタラクティブなフローグラフを構築します。

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

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

読み取り専用

カスタムノードの作成

まず、カスタム入力ノード(NumberInput.js)を作成し、そのインスタンスを3つ追加します。制御された<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;

次に、各カラーチャネルに1つのターゲットハンドルと、結果の色を表示する背景を持つ新しいカスタムノード(ColorPreview.js)を追加します。mix-blend-mode: 'difference';を使用して、テキストの色を常に読みやすくすることができます。

単一のノードに同じ種類のハンドルが複数ある場合は、それぞれに個別のIDを付けることを忘れないでください。

また、入力ノードからカラーノードへのエッジを、initialEdges配列に追加しましょう。

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

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

読み取り専用

データの計算

入力ノードからカラーノードにデータを取得するにはどうすればよいでしょうか?これは、この目的のために作成された2つのフックを含む、2段階のプロセスです。

  1. updateNodeDataコールバックを使用して、各数値入力値をノードのdataオブジェクト内に保存します。
  2. useHandleConnectionsを使用してどのノードが接続されているかを調べ、次にuseNodesDataを使用して、接続されたノードからデータを受信します。

ステップ1:データオブジェクトへの値の書き込み

まず、initialNodes配列のdataオブジェクト内に入力ノードの初期値を追加し、入力ノードの初期状態として使用します。次に、useReactFlowフックから関数updateNodeDataを取得し、入力が変更されるたびに新しい値でノードのdataオブジェクトを更新するために使用します。

デフォルトでは、updateNodeDataに渡すデータは、古いデータオブジェクトとマージされます。これにより、部分的な更新が容易になり、{...data}の追加を忘れた場合に役立ちます。代わりにオブジェクトを置き換えるオプションとして、{ replace: true }を渡すことができます。

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

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

読み取り専用
⚠️

入力フィールドを扱う場合、ノードのdataオブジェクトをUI状態として直接使用することは避けてください。

データオブジェクトの更新に遅延が発生し、カーソルが不規則にジャンプして、不要な入力につながる可能性があります。

ステップ2:接続されたノードからのデータの取得

まず、useHandleConnectionsフックを使用して各ハンドルのすべての接続を特定し、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)を追加します。色の知覚される明るさ(0が最も暗く、255が最も明るい)を計算するために、相対輝度式 luminance = 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b を使用できます。128以上のものはすべて明るい色であると仮定できます。

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

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

読み取り専用

条件分岐

知覚される明るさに基づいてフローで異なるパスを取りたい場合はどうすればよいでしょうか?明るさノードに2つのソースハンドル lightdark を与え、ノードの data オブジェクトをソースハンドルIDごとに分割します。複数のソースハンドルがある場合に、各ソースハンドルのデータを区別するために必要です。

しかし、「異なるルートを取る」とはどういう意味でしょうか?1つの解決策としては、ターゲットハンドルに接続された null または undefined のデータは「停止」とみなすと仮定することです。この場合、明るい色であれば 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 を使用します。

例えば、undefined である入力データを「停止」として解釈することで、分岐を実装できます。余談ですが、分岐もあるほとんどのフローグラフでは、ノードのトリガーとノードにフックアップされた実際のデータは通常分離されています。Unreal Engine のブループリントが良い例です。

最後に、始める前に注意すべきことがもう1つあります。今やったようにアイデアを混在させるのではなく、すべてのノードデータを構造化する一貫した方法を見つける必要があります。例えば、ハンドルIDごとにデータを分割する作業を開始する場合、複数のハンドルを持っているかどうかに関係なく、すべてのノードでそれを行う必要があります。フロー全体でデータの構造について仮定できると、作業が非常に楽になります。