はじめに
reactflowはfigmaやmiroのようなGUIでポチポチで図を作成することを容易にするライブラリです。これを使ってみてどんなことができるか試します。
公式ドキュメントは充実しているので、詳細はそちらを読めばわかると思いつつ、この記事ではざっくりとreactflowの使い方の紹介と、ドキュメントではわかりにくい部分について補足的に紹介します。
github repository: https://github.com/xyflow/xyflow
今回作成したサンプル: https://github.com/katamotokosuke/reactflow-sample-for-qiita
reactflowの導入
すでにreactアプリがある場合は省略可能ですが、無い場合新たに作ります。
npx create-react-app my-reactflow-app --template typescript
上記を実行してできたディレクトリに移動して、reactflowをインストールします。
https://reactflow.dev/learn/getting-started/installation-and-requirements
npm install reactflow
かんたんなダイアグラムを作成してみる
これまでの導入でreactflowは使える状態になっているので、早速動かせないnodeを2つとそれをつなぐedgeを1つ描画しています。
import React from 'react';
import './App.css';
import {ReactFlow} from "reactflow";
// このcssのimportがないと、reactflowの描画が崩れるので必要
import 'reactflow/dist/style.css';
function App() {
const initialNodes = [
{ id: '1', position: { x: 0, y: 0 }, data: { label: '1' } },
{ id: '2', position: { x: 0, y: 100 }, data: { label: '2' } },
];
// source: edgeの元となるnodeのidを指定
// target: edgeの先となるnodeのidを指定
// id: reactflowのドキュメントは sourceEdgeId-targetEdgeId という形式で書かれているが、多分なんでもOK
const initialEdges = [{ id: '1-2', source: '1', target: '2' }];
return (
<div style={{ width: '100vw', height: '100vh' }}>
<ReactFlow nodes={initialNodes} edges={initialEdges} />
</div>
);
}
export default App;
とても簡単にそれらしきものができました
カスタムノード、カスタムエッジを作ってみる
現時点でまだノードやエッジは動かせないですが、次に自分たちでカスタマイズ可能なノードとエッジを作成していきます。
まずはノードから
カスタムノードは適当に下記のように作成してみました。
import {Handle, Node, NodeProps, Position} from 'reactflow';
export type NodeData = {
label: string;
}
export function MyNode({ id, data }: NodeProps<NodeData>) {
return (
<>
<Handle
id={`${id}-target`}
type="target"
position={Position.Top}
/>
<div
style={{
background: 'green',
padding: 10,
borderRadius: 5,
width: 20,
height: 20,
}}>
<strong>{data.label}</strong>
</div>
<Handle
id={`${id}-source`}
type="source"
position={Position.Bottom}
/>
</>
);
}
カスタムノードに渡される引数はドキュメントに記載されています。initialNodesを作るときやnodeを追加するときにプロパティによしなな値を渡すことでnodeに情報を渡すことが可能です。
ノードをカスタムノードとして取り扱うためにApp.tsxの部分を修正します。とは言ってもinitialNodeにtypeを付与することと、Reactflowコンポーネントにどのカスタムノードを利用するかの情報を渡すだけです。
修正後App.tsx
import React, {useMemo} from 'react';
import './App.css';
import {Background, Controls, ReactFlow} from "reactflow";
import 'reactflow/dist/style.css';
import {MyNode} from "./Node";
function App() {
const nodeTypes = useMemo(() => ({ myNode: MyNode }), []);
// typeにmyNodeを渡すことでそのノードがどのカスタムノードを期待しているかを渡す
const initialNodes = [
{ id: '1', position: { x: 0, y: 0 }, data: { label: '1' }, type: 'myNode'},
{ id: '2', position: { x: 0, y: 100 }, data: { label: '2' }, type: 'myNode' },
];
const initialEdges = [{ id: '1-2', source: '1', target: '2' }];
return (
<div style={{ width: '100vw', height: '100vh' }}>
<ReactFlow
nodes={initialNodes}
edges={initialEdges}
// nodeTypesにこのフロー図で利用するカスタムノードの情報を渡す
nodeTypes={nodeTypes}
>
<Background />
<Controls />
</ReactFlow>
</div>
);
}
export default App;
標準ノードより見た目が悪くなった気がしますが、オリジナルのノードができました!
カスタムエッジ
エッジもカスタムノードと同様に自作のエッジコンポーネントを作成してtypeをReactflowコンポーネントにわたす構成です。
今回作成したEdgeは以下です。
import {BaseEdge, EdgeProps, getSmoothStepPath} from 'reactflow';
export default function MyEdge({ id, sourceX, sourceY, targetX, targetY, markerEnd }: EdgeProps) {
const [edgePath] = getSmoothStepPath({
sourceX,
sourceY,
targetX,
targetY,
});
return (
<>
<BaseEdge
id={id}
path={edgePath}
markerEnd={markerEnd}
style={{
stroke: 'red',
strokeWidth: 2,
}}
/>
</>
);
}
今回はgetSmoothStepPathを利用しましたが、標準で4つのedgeが用意されているようです。
※getBezierPathとの違いがよくわからなかったです
カスタムノードはできたので、Reactflowコンポーネントにカスタムノードのtype情報を渡します。
App.tsx
import React, {useMemo} from 'react';
import './App.css';
import {Background, Controls, MarkerType, ReactFlow} from "reactflow";
import 'reactflow/dist/style.css';
import {MyNode} from "./Node";
import MyEdge from "./Edge";
function App() {
const nodeTypes = useMemo(() => ({ myNode: MyNode }), []);
const edgeTypes = useMemo(() => ({ myEdge: MyEdge}), []);
const initialNodes = [
{ id: '1', position: { x: 0, y: 0 }, data: { label: '1' }, type: 'myNode'},
{ id: '2', position: { x: 50, y: 100 }, data: { label: '2' }, type: 'myNode' },
];
#### typeにmyEdgeを指定することでカスタムエッジを利用することを指定
const initialEdges = [
{ id: '1-2', source: '1', target: '2', type: 'myEdge', markerEnd: {type: MarkerType.ArrowClosed, width: 10, height: 10, color: '#FF0072'}}
];
return (
<div style={{ width: '100vw', height: '100vh' }}>
<ReactFlow
nodes={initialNodes}
edges={initialEdges}
nodeTypes={nodeTypes}
### このフロー図で使うedgeTypeを渡す
edgeTypes={edgeTypes}
>
<Background />
<Controls />
</ReactFlow>
</div>
);
}
export default App;
これでエッジも独自のものになりました!今回シンプルなカスタムノードとカスタムエッジを作成しましたが、html, css(svgでも可)で表現可能な範囲では自由にノードエッジを描けると思います。
もう少し複雑なフロー図を描いてみよう
今までの記述だとノードやエッジは動かせないので、動かせるようしてみましょう。とはいってもreactflow標準で基本的に備え付けられている機能なので簡単導入可能です。
この記事では以下の仕様に準拠したフロー図を作成してみます。
- エッジを追加できるようにする
- 自己参照と循環参照を禁止する
まずはノードとエッジを接続可能に、ドラッグ可能にする
ReactflowコンポーネントにonNodesChangeとonEdgeChangesのコールバックを渡すことで、そのインタラクションを加えます。
https://reactflow.dev/api-reference/hooks/use-nodes-state
https://reactflow.dev/api-reference/hooks/use-edges-state
useNodesState, useEdgeStateにデフォルトのnode, edgesを渡すとデフォルトのコールバック関数が得られます。これをReactflowコンポーネントにわたすことで変更があるたびにこのコールバックが呼ばれ、ドラッグすることで変わったxPoxやyPosがノードに伝えられます。
あとノード間を接続できるようにするためにはReactflowコンポーネントにonConnectコールバックを渡すようにする必要があります。onConnectコールバックはノード間がエッジで結ばれたときに呼ばれるコールバックです。繋げられた新たなedgeを追加する処理を書くと良さそうです。
一応変更したApp.tsxは以下
App.tsx
import React, {useMemo} from 'react';
import './App.css';
import {Background, Controls, MarkerType, ReactFlow, useEdgesState, useNodesState} from "reactflow";
import 'reactflow/dist/style.css';
import {MyNode} from "./Node";
import MyEdge from "./Edge";
function App() {
const nodeTypes = useMemo(() => ({ myNode: MyNode }), []);
const edgeTypes = useMemo(() => ({ myEdge: MyEdge}), []);
const initialNodes = [
{ id: '1', position: { x: 0, y: 0 }, data: { label: '1' }, type: 'myNode'},
{ id: '2', position: { x: 50, y: 100 }, data: { label: '2' }, type: 'myNode' },
### 検証のためつながっていないノードを用意
{ id: '3', position: { x: 150, y: 100 }, data: { label: '3' }, type: 'myNode' },
];
### 新たな接続をedgesに加えるようにする。
const onConnect = (params: Connection) => setEdges((eds) => addEdge(params, eds));
const initialEdges = [
{ id: '1-2', source: '1', target: '2', type: 'myEdge', markerEnd: {type: MarkerType.ArrowClosed, width: 10, height: 10, color: '#FF0072'}}
];
### ここで標準のonNodesChange, onEdgeChangeを取得して、Reactflowコンポーネントにわたす
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
return (
<div style={{ width: '100vw', height: '100vh' }}>
<ReactFlow
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
### コールバックを渡す
onConnect={onConnect}
### 新たに追加されるedgeについてデフォルトのオプションを与える
### 新たに追加されるedgeもカスタムエッジであることを指定した
defaultEdgeOptions={{
type: 'myEdge',
markerEnd: {
type: MarkerType.ArrowClosed,
width: 10,
height: 10,
color: '#FF0072'
}
}}
>
<Background />
<Controls />
</ReactFlow>
</div>
);
}
export default App;
エッジの接続を制限する
下記の仕様を実装していきます。
- 自己参照を禁止
- 循環参照を禁止する
下記の2つを利用できそうです。
handle用
フロー全体用
今回はフロー全体用のコールバックでどちらもできそうなので、そちらを利用して実装していきます。
const isValidConnection = useCallback((connection: Connection) => {
const nodes = getNodes();
const edges = getEdges();
const target = nodes.find((node: Node) => node.id === connection.target);
if (!target) return false;
const hasCycle = (node: Node, visited = new Set()) => {
if (visited.has(node.id)) return false;
visited.add(node.id);
for (const outgoer of getOutgoers(node, nodes, edges)) {
if (outgoer.id === connection.source) return true;
if (hasCycle(outgoer, visited)) return true;
}
};
if (target.id === connection.source) return false; // これは自己参照防止
return !hasCycle(target);
},
[getNodes, getEdges],
);
上記の関数をReactflowコンポーネントに渡すことで接続されたときに検証し、この関数の結果がfalseの場合、onConnectコールバックを呼ばないという流れになります。
今回利用したreactflow関数の解説
- getNodes: 現在保持しているノードの一覧を取得している
- getEdges: 現在保持しているエッジの一覧を取得する
- getOutgoers: 第一引数で指定したノードからでていく(直接繋がれている次のノード)一覧を取得する
hasCycle関数を再帰的に使用して、新しい接続がグラフ内でサイクルを作成するかどうかをチェックしています。この関数は、訪問済みのノードをvisitedセットに記録しながら、getOutgoersを使用してターゲットノードから出ていくノードを再帰的に探索します。もし接続のソースノードにたどり着くか、再帰のどの段階かでサイクルが発見された場合、接続は無効とする関数となります。
実際の動きを見てみる
ここまでのまとめ
✅カスタムノード、エッジの作成をして、かんたんなインタラクションが得られました
✅作りたい機能があれば自由に追加できる環境にはなったと思います
NEXT: 詳細はドキュメントを見るのが一番ですが、これからの章ではドキュメントでは少々説明が不足してそうな部分について実際の動作を見ながらどう動くのかを理解していきます
Reactflowのコンポーネント周り
概ねドキュメントは充実していますが、部分的にどのタイミングでイベントトリガーが発火するかや、順序については少し説明が不足しているように見えました。ここではドキュメントを補足するような形でコンポーネントを見ていきます。
※ドキュメントに説明がある部分については省きます。関数名と引数だけ書かれてだけかつコールバック名だけだと、なにのことかよくわからない部分、呼ばれる順番がわからないものついてのみ記載しています。
Common Props
- onNodesChange : ノードの変更を検出するために使用されるコールバック関数。ノードに変更(ドラッグ、選択など)があればこのコールバックが呼ばれる。ノードの変更でなにかしたい場合使えそう。複数同時に変わるケース(複数選択)もあるので配列となっている。useNodesStateから標準のonNodesChange関数が取れるので、特に何もない場合、そのまま渡すと良い。 https://reactflow.dev/api-reference/types/node-change
- onEdgesChange : エッジの変更を検出するために使用されるコールバック関数。エッジが選択されたり削除されたりしたら呼ばれる。ノードと同じ理由で配列。 こちらもonEdgesStateから取れる関数を渡すといい感じに動作する。 https://reactflow.dev/api-reference/types/edge-change
- onConnect: 新しい接続が作成されたときに呼び出されるコールバック関数です
各コールバック関数でconsole.logをすると
Connection Events
- onConnect: 前述の通り。新しい接続が作成されたときに呼び出されます。引数として、接続に関する情報をオブジェクトで受け取ります。 isValidConnectionの検証後、trueの場合、接続が成立し、その後onConnectが呼ばれる。
- onConnectStart: 新しい接続の作成が開始されたときに呼び出されます。引数として、接続の開始点となるノードとイベントオブジェクトを受け取ります。 その名の通り、エッジを伸ばしたときに呼ばれる
- onConnectEnd: 新しい接続の作成が終了したときに呼び出されます。引数として、接続の終点となるノードとイベントオブジェクトを受け取ります。 onConnectのあとに呼ばれる。また、接続が成立しなくても呼ばれる。
- onClickConnectStart: ノード上で接続の作成が開始されたときに呼び出されます。引数として、クリックされたノードとイベントオブジェクトを受け取ります。handleをクリックしたときに呼ばれる。
- onClickConnectEnd: ノード上で接続の作成が終了したときに呼び出されます。引数として、クリックされたノードとイベントオブジェクトを受け取ります。自分の検証ではどのタイミングで呼ばれるかわかりませんでした。
- isValidConnection: 新しい接続が作成可能かどうかを判定するために呼び出されます。引数として、接続の開始点となるノード、接続の終点となるノード、接続の種類を受け取ります。source側のハンドルにマウスをhoverしたタイミングで呼ばれる。この関数がfalseの場合、mouseupしても接続は成立しない。
各コールバック関数でconsole.logをすると
Selection Events
- onSelectionChange: 選択範囲が変更されたときに呼び出されます。引数として、選択されたノードとエッジの情報を含むオブジェクトを受け取ります。 選択しているnode, edgeが増減するたびに呼ばれる。また、ドラッグして座標が変わったタイミングでも呼ばれます。選択中のものが変更されるととりあえず呼ばれると思います。onNodesChange、onEdgesChangeのときと同じ。
- onSelectionDragStart: 選択範囲のドラッグが開始されたときに呼び出されます。引数として、マウスイベントオブジェクトと、選択されているノードの配列を受け取ります。その名の通り、選択中のものをドラッグしたタイミングで呼ばれます。一回しか呼ばれないです。
- onSelectionDrag: 選択範囲がドラッグされている間に呼び出されます。引数として、マウスイベントオブジェクトと、選択されているノードの配列を受け取ります。
- onSelectionDragStop: 選択範囲のドラッグが終了したときに呼び出されます。引数として、マウスイベントオブジェクトと、選択されているノードの配列を受け取ります。一回しか呼ばれないです。
- onSelectionStart: 選択が開始されたときに呼び出されます。引数を受け取りません。一回しか呼ばれないです。
- onSelectionEnd: 選択が終了したときに呼び出されます。引数を受け取りません。一回しか呼ばれないです。
各コールバック関数でconsole.logをすると
まとめ
まず、はじめにreactflowを使った簡単なダイアグラムの作成をしました。
その後カスタムの基本的な部分を取り扱いました。そして、これを拡張できるようreactflowのコールバックに焦点を当てた紹介をしました。
機能としてはまだまだあるので、興味が出てきた人は遊んでみると良いと思います。
それではよきポチポチライフを!