ReactFlowで有向グラフを描画してみた

## React Flow (xyflow) とは

> Wire Your Ideas with React Flow
> A customizable React component for building node-based editors and interactive diagrams

簡単に言えば、MiroやMURALのようなホワイトボードアプリのようなものを作るベースとなるライブラリです。
Reactアプリにコンポーネントを組み込んで利用します。

- https://reactflow.dev/
- https://github.com/xyflow/xyflow

## モチベーション

任意の有向グラフを描画するにあたって、良いライブラリがないか探していました。
最初はグラフ表示だけを考えていましたが、メンテナンス状況が良好で今時っぽいいい感じのものが見つけられなかったことと、後にグラフの編集機能も実装することになると思い、その際のUXを考えた上でこちらを選定しました。

## 概要と学び方

まずは公式の [Quickstart](https://reactflow.dev/learn) から初めて、描画のノリを学びます。
以下、公式のサンプルより抜粋。

```tsx
export default function App() {
  const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
  const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
 
  const onConnect = useCallback(
    (params) => setEdges((eds) => addEdge(params, eds)),
    [setEdges],
  );
 
  return (
    <div style={{ width: '100vw', height: '100vh' }}>
      <ReactFlow
        nodes={nodes}
        edges={edges}
        onNodesChange={onNodesChange}
        onEdgesChange={onEdgesChange}
        onConnect={onConnect}
      >
        <Controls />
        <MiniMap />
        <Background variant="dots" gap={12} size={1} />
      </ReactFlow>
    </div>
  );
}
```

ReactFlowコンポーネントに、描画するノードとエッジを渡すことで描画されます。

- ノードはid, position(x,y), data(label) を持ち、初期状態では指定されたポジションに配置されます
- エッジはid, source(id), target(id)を持ち、sourceとtargetにより自動的に線が引かれます

ノードはドラッグすることで位置を変更できます。また、タイプによって、入出力の節点が異なります。
エッジはノードの節点同士を接続できます。

ここでは簡単な紹介にとどめますが、機能が多くあり、機能紹介やサンプルもたくさんあるので、非常に学びやすくなっています。
[Learn](https://reactflow.dev/learn) と [Example](https://reactflow.dev/examples) を一通り読み、その上で [Reference](https://reactflow.dev/api-reference) を読んでいくと非常に捗りました。

## 利用してみた機能

- Components
  - Background ... 描画エリアの背景
  - Controls ... 拡大・縮小等のコントロールパネル
  - MiniMap ... 描画エリアのどこにいるかわかるあれ
  - Panel ... 描画エリア上の任意の場所に任意のコンポーネントを配置できる
- Hooks
  - useNodesState ... useState的なの。ノードの変更に対するコールバックも提供される
  - useEdgeState ... 上記のエッジ版
  - useOnSelectionChange ... ノードの選択/解除をフックできる
  - useReactFlow ... D&Dでノードを配置する際、そのポジションを取得するのに（Instanceを)利用
- Utils
  - addEdge ... 節点接続時に利用することでエッジを追加できる
  - etc...

## 作ってみた機能

### パネルからD&Dでノード配置

参考 : https://reactflow.dev/examples/interaction/drag-and-drop

サンプルコードそのままで動くのですが、ReactFlowInstance周りでVSCode上で[エラー](https://reactflow.dev/learn/troubleshooting#001)になります。
サンプルではuseState(初期値null)で生成していますが、これに起因しています。
[useReactFlow()](https://reactflow.dev/api-reference/hooks/use-react-flow) で生成するように変更することで解消できます。

### カスタムノード

参考 : https://reactflow.dev/learn/customization/custom-nodes

これを知っておくことで、デフォルトのノードがどのように実装されているかも理解できて良いです。
また、真面目にReactFlowを利用するなら、実装や責務がシンプルになるので必須かと思います。

一点注意するところがあるとすれば、公式ドキュメントにもありますが、
Handle側で接続時のバリデーションを頑張ると[パフォーマンス的によろしくない](https://reactflow.dev/api-reference/components/handle#is-valid-connection)ようなので最低限にした方がよさそうです。

### ノードのラベル変更

参考 : https://reactflow.dev/examples/nodes/update-node

基本方針は選択されたノードに対して変更する口を与える、入力に応じて実際に変更する実装を行います。

#### デフォルトノードの場合

選択されたノードの取得は [useOnSelectionChange()](https://reactflow.dev/api-reference/hooks/use-on-selection-change) を利用します。

データに保持しているラベルを変更しようにも、デフォルトノードに良いI/Fがありません。
ノード選択時にパネルを表示し、パネルの入力欄からラベルを変更させます。

#### カスタムノードの場合

ノードに直接入力エリアを配置できるので、あとはどうにでもできます。
こちらでは選択されたノードを判定する必要は特にないので、上記フックは不要です。

こちらは実装がカスタムノードに集約されるのが良いです。

### ノードの保存とクリア

参考 : https://reactflow.dev/examples/interaction/save-and-restore

配置したノードを保存またはクリアするボタンを配置します。
クリアはシンプルで、setNodes, setEdgesでから配列を指定するだけになります。

保存では、例えばBackendのAPIを叩いて状態を保存すると思います。
現在のノード一覧からAPIを叩くためのリソースを生成して、実際にAPIを叩く処理が必要になるため、やや複雑になります。

また、バリデーション(Frontend側)をこのタイミングで行うとさらに複雑になります。
バリデーションは基本的にエッジの接続時に行い、保存時はデータ整形と簡単なチェック(ラベルの長さ等)に留めると良いと思います。

### バリデーション

参考 : https://reactflow.dev/examples/interaction/validation

エッジ接続時に接続可能かを判定します。
NGの場合エッジを接続できませんし、エッジの接続する際も節点に吸い付くような動きをしなくなります。

ReactFlowコンポーネントのisValidConnectionを利用しますが、前述の通り、カスタムノードのHandleコンポーネントでも可能です。
ノード自体のバリデーションはHandle側に任せ、全体としての整合をReactFlow側に任せると良いのかなという印象です。

ちなみに、Handleにはタイプがsourceとtargetがあり、これを意識してバリデーションを書く必要があります。
これが結構混乱するので、Handleを複数(3個以上)設置するような場合は注意が必要かと思いました。

## 所感

ドキュメントもサンプルも充実していて初めて使う分にもスムーズに利用できました。
前述のように、やりたいことに対して何かしらのドキュメントが見つかりますし、API Reference含め良いライブラリの印象です。

辛かったのは、初期配置のノードのポジショニングです。
初期表示するノードとエッジ自体を生成するのは苦ではないですが、ポジショニングを保存しなかったり整頓する場合、XY座標を良い感じに配置するロジックを考えるのが大変でした。
ただ、これ自体はReactFlow直接の話ではないので、ライブラリとしては利用していてとても楽しかったです。