33
34

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ReactFlowではじめるダイアグラムポチポチライフ

Posted at

はじめに

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つ描画しています。

App.tsx
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;

とても簡単にそれらしきものができました

スクリーンショット 2024-03-09 午前11.24.20.png

カスタムノード、カスタムエッジを作ってみる

現時点でまだノードやエッジは動かせないですが、次に自分たちでカスタマイズ可能なノードとエッジを作成していきます。

まずはノードから

カスタムノードは適当に下記のように作成してみました。

Node.tsx

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
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;

標準ノードより見た目が悪くなった気がしますが、オリジナルのノードができました!

スクリーンショット 2024-03-09 午後3.20.46.png

カスタムエッジ

エッジもカスタムノードと同様に自作のエッジコンポーネントを作成してtypeをReactflowコンポーネントにわたす構成です。

今回作成したEdgeは以下です。

Edge.tsx
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
    スクリーンショット 2024-03-09 午後3.29.03.png

  • getSimpleBezierPath
    スクリーンショット 2024-03-09 午後3.29.34.png

※getBezierPathとの違いがよくわからなかったです

  • getSmoothStepPath
    スクリーンショット 2024-03-09 午後3.30.50.png

  • getStraightPath
    スクリーンショット 2024-03-09 午後3.31.14.png

カスタムノードはできたので、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;

スクリーンショット 2024-03-10 午前8.55.16.png

これでエッジも独自のものになりました!今回シンプルなカスタムノードとカスタムエッジを作成しましたが、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
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;

draggable2.gif

エッジの接続を制限する

下記の仕様を実装していきます。

  • 自己参照を禁止
  • 循環参照を禁止する

下記の2つを利用できそうです。

handle用

フロー全体用

今回はフロー全体用のコールバックでどちらもできそうなので、そちらを利用して実装していきます。

App.tsx
    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を使用してターゲットノードから出ていくノードを再帰的に探索します。もし接続のソースノードにたどり着くか、再帰のどの段階かでサイクルが発見された場合、接続は無効とする関数となります。

実際の動きを見てみる

draggable2.gif

ここまでのまとめ

✅カスタムノード、エッジの作成をして、かんたんなインタラクションが得られました
✅作りたい機能があれば自由に追加できる環境にはなったと思います
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をすると

draggable2.gif

Connection Events

  • onConnect: 前述の通り。新しい接続が作成されたときに呼び出されます。引数として、接続に関する情報をオブジェクトで受け取ります。 isValidConnectionの検証後、trueの場合、接続が成立し、その後onConnectが呼ばれる。
  • onConnectStart: 新しい接続の作成が開始されたときに呼び出されます。引数として、接続の開始点となるノードとイベントオブジェクトを受け取ります。 その名の通り、エッジを伸ばしたときに呼ばれる
  • onConnectEnd: 新しい接続の作成が終了したときに呼び出されます。引数として、接続の終点となるノードとイベントオブジェクトを受け取ります。 onConnectのあとに呼ばれる。また、接続が成立しなくても呼ばれる。
  • onClickConnectStart: ノード上で接続の作成が開始されたときに呼び出されます。引数として、クリックされたノードとイベントオブジェクトを受け取ります。handleをクリックしたときに呼ばれる。
  • onClickConnectEnd: ノード上で接続の作成が終了したときに呼び出されます。引数として、クリックされたノードとイベントオブジェクトを受け取ります。自分の検証ではどのタイミングで呼ばれるかわかりませんでした。
  • isValidConnection: 新しい接続が作成可能かどうかを判定するために呼び出されます。引数として、接続の開始点となるノード、接続の終点となるノード、接続の種類を受け取ります。source側のハンドルにマウスをhoverしたタイミングで呼ばれる。この関数がfalseの場合、mouseupしても接続は成立しない。

各コールバック関数でconsole.logをすると

draggable2.gif

Selection Events

  • onSelectionChange: 選択範囲が変更されたときに呼び出されます。引数として、選択されたノードとエッジの情報を含むオブジェクトを受け取ります。 選択しているnode, edgeが増減するたびに呼ばれる。また、ドラッグして座標が変わったタイミングでも呼ばれます。選択中のものが変更されるととりあえず呼ばれると思います。onNodesChange、onEdgesChangeのときと同じ。
  • onSelectionDragStart: 選択範囲のドラッグが開始されたときに呼び出されます。引数として、マウスイベントオブジェクトと、選択されているノードの配列を受け取ります。その名の通り、選択中のものをドラッグしたタイミングで呼ばれます。一回しか呼ばれないです。
  • onSelectionDrag: 選択範囲がドラッグされている間に呼び出されます。引数として、マウスイベントオブジェクトと、選択されているノードの配列を受け取ります。
  • onSelectionDragStop: 選択範囲のドラッグが終了したときに呼び出されます。引数として、マウスイベントオブジェクトと、選択されているノードの配列を受け取ります。一回しか呼ばれないです。
  • onSelectionStart: 選択が開始されたときに呼び出されます。引数を受け取りません。一回しか呼ばれないです。
  • onSelectionEnd: 選択が終了したときに呼び出されます。引数を受け取りません。一回しか呼ばれないです。

各コールバック関数でconsole.logをすると

draggable2.gif

まとめ

まず、はじめにreactflowを使った簡単なダイアグラムの作成をしました。
その後カスタムの基本的な部分を取り扱いました。そして、これを拡張できるようreactflowのコールバックに焦点を当てた紹介をしました。

機能としてはまだまだあるので、興味が出てきた人は遊んでみると良いと思います。
それではよきポチポチライフを!

33
34
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
34

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?