React Flow使ってみた

# 初めに
「いい感じのUIでフローを作成出来るようにしたい！」と要望が上がった際に、React Flowを使ってみました。
試した結果を記事にまとめました。

https://reactflow.dev/

# やってみた
1\.[まずは基本の型](#まずは基本の型)
2\.[カスタムノード](#カスタムノード)
3\.[線の付け替え・削除](#線の付け替え削除)

## まずは基本の型
まずは公式が用意している基本のユースケースを試してみます。

https://reactflow.dev/docs/quickstart/

```javascript:Flow.js
import { useCallback } from 'react';
import ReactFlow, {
  MiniMap,
  Controls,
  Background,
  useNodesState,
  useEdgesState,
  addEdge,
} from 'reactflow';
import 'reactflow/dist/style.css';

const initialNodes = [
  {
    id: 'start',
    type: 'input',
    position: { x: 0, y: 0 },
    data: { label: 'start' },
  },
  {
    id: 'middle',
    position: { x: 0, y: 100 },
    data: { label: 'middle' },
  },
  {
    id: 'end',
    type: 'output',
    position: { x: 0, y: 200 },
    data: { label: 'end' },
  },
];

const initialEdges = [
  { id: 'e1', source: 'start', target: 'middle' },
  { id: 'e2', source: 'middle', target: 'end' },
];

function Flow() {
  const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
  const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);

  const onConnect = useCallback((params) => setEdges((eds) => addEdge(params, eds)), [setEdges]);

  return (
    <ReactFlow
      nodes={nodes}
      edges={edges}
      onNodesChange={onNodesChange}
      onEdgesChange={onEdgesChange}
      onConnect={onConnect}
      fitView
    >
      <MiniMap />
      <Controls />
      <Background />
    </ReactFlow>
  );
}

export default Flow;
```
![base.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952834/5d8dd1a3-6cac-5e8d-78b7-d36e412a93e3.png)


それっぽいのは出ました。
ここから修正を加えていきます。

## カスタムノード
ノードをカスタムします。
今回実行したのは以下の通りです。
* ノードのバックグランド変更(条件付き)
* ポートを大きく
* ドラッグ範囲の変更

```javascript:Flow.js
import { useCallback } from 'react';
import ReactFlow, {
  MiniMap,
  Controls,
  Background,
  useNodesState,
  useEdgesState,
  addEdge,
} from 'reactflow';
import 'reactflow/dist/style.css';

import CustomNode from './CustomNode';

const initialNodes = [
  {
    id: 'start',
    type: 'input',
    position: { x: 0, y: 0 },
    data: { label: 'start' },
  },
  {
    id: 'middle1',
    position: { x: -100, y: 100 },
    data: { label: 'middle1' },
  },
  {
    id: 'middle2',
    type: 'custom',   // カスタムノードを適用
    dragHandle: '.custom-drag-handle',    // ドラッグ領域の指定
    position: { x: 100, y: 100 },
    data: { label: 'middle2' },
  },
  {
    id: 'end',
    type: 'output',
    position: { x: 0, y: 200 },
    data: { label: 'end' },
  },
];

const nodeTypes = {
  custom: CustomNode,
};

const initialEdges = [
  { id: 'e1', source: 'start', target: 'middle' },
  { id: 'e2', source: 'middle', target: 'end' },
];

function Flow() {
  const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
  const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);

  const onConnect = useCallback((params) => setEdges((eds) => addEdge(params, eds)), [setEdges]);

  return (
    <ReactFlow
      nodes={nodes}
      edges={edges}
      onNodesChange={onNodesChange}
      onEdgesChange={onEdgesChange}
      onConnect={onConnect}
      nodeTypes={nodeTypes}
      fitView
    >
      <MiniMap />
      <Controls />
      <Background />
    </ReactFlow>
  );
}

export default Flow;
```

```javascript:CustomNode.js
import { Handle, Position, useStore } from 'reactflow';

const connectionNodeIdSelector = (state) => state.connectionNodeId;

export default function CustomNode({ id, data }) {
  const connectionNodeId = useStore(connectionNodeIdSelector);
  const isTarget = connectionNodeId && connectionNodeId !== id;

  return (
    <div className="customNode">
      <div
        className="customNodeBody"
        style={{
          borderStyle: isTarget ? 'dashed' : 'solid',
          backgroundColor: isTarget ? '#ffcce3' : '#ccd9f6',
        }}
      >
        <Handle
          className="port"
          position={Position.Top}
          type="target"
        />
        <Handle
          className="port"
          position={Position.Bottom}
          type="source"
        />
        {data.label}
        <span className="custom-drag-handle"/>
      </div>
    </div>
  );
}

```

```css:index.css
/* ポートの色、サイズ */
.react-flow__handle.port {
  width: 10px;
  height: 10px;
  border-radius: 3px;
  background-color: red;
}

/* カスタムノード */
.customNodeBody {
  width: 150px;
  height: 18px;
  padding: 10px;
  border: 1px solid black;
  overflow: hidden;
  border-radius: 10px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-weight: bold;
}

/* ドラッグ */
.custom-drag-handle {
  width: 10px;
  height: 10px;
  position: absolute;
  background-color: teal;
  top: 10%;
  right: 10%;
};
```

![custom_node.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952834/96219be4-1a6b-7138-99a5-900db691d2e9.gif)


カスタムノードから線をつなぐ際に色が変わります。
また、ノードのドラッグは緑の範囲でのみ可能になります。

## 線の付け替え削除
ポート接続の変更、削除が出来ないため修正します。
それぞれ何をしているかは以下を参考にしてください。

[snapToGrid](https://reactflow.dev/docs/api/react-flow-props/#snaptogrid)
[useRef](https://reactjs.org/docs/hooks-reference.html#useref)
[updatable-edge](https://reactflow.dev/docs/examples/edges/updatable-edge/)

```javascript:Flow.js
  const onConnect = useCallback((params:any) => setEdges((eds) => addEdge(params, eds)), [setEdges]);

  // -- ここから追加 --
  const edgeUpdateSuccessful = useRef(true);

  const onEdgeUpdateStart = useCallback(() => {
    edgeUpdateSuccessful.current = false;
  }, []);

  const onEdgeUpdate = useCallback((oldEdge:any, newConnection:any) => {
    edgeUpdateSuccessful.current = true;
    setEdges((els) => updateEdge(oldEdge, newConnection, els));
  }, []);

  const onEdgeUpdateEnd = useCallback((_:any, edge:any) => {
    if (!edgeUpdateSuccessful.current) {
      setEdges((eds) => eds.filter((e) => e.id !== edge.id));
    }

    edgeUpdateSuccessful.current = true;
  }, []);
  // -- ここまで追加 --

  return (
    <ReactFlow
      nodes={nodes}
      edges={edges}
      onNodesChange={onNodesChange}
      onEdgesChange={onEdgesChange}
      // -- ここから追加 --
      snapToGrid
      onEdgeUpdate={onEdgeUpdate}
      onEdgeUpdateStart={onEdgeUpdateStart}
      onEdgeUpdateEnd={onEdgeUpdateEnd}
      // -- ここまで追加 --
      onConnect={onConnect}
      className="touchdevice-flow"
      fitView
    >
      <MiniMap />
      <Controls />
      <Background />
    </ReactFlow>
  );
```
![port_change.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952834/a5dacd69-5053-b826-37d1-b4fd2aee3a56.gif)

# まとめ
React Flowを触ってみたが、まだまだ出来ることがたくさんあり、ドキュメントも充実してそうな印象です。
興味があれば触ってみてはいかがでしょうか。