React Flowを使って複雑なUIを簡単に実現する方法

はい、承知いたしました。React Flowを使って複雑なUIを簡単に実現する方法に関する詳細な記事(約5000語)を作成します。


React Flowを使って複雑なUIを簡単に実現する方法

はじめに

現代のウェブアプリケーションにおいて、複雑なシステムやプロセスを視覚的に表現する必要性はますます高まっています。ワークフロー、データパイプライン、ノードベースのプログラミングインターフェース、組織図、ネットワーク構成図など、様々な分野で要素間の関係性をフロー図として表示し、操作できるUIが求められています。

このようなフロー図UIの開発は、一見シンプルに見えて非常に複雑です。要素(ノード)の配置、接続線(エッジ)の描画、ズームとパン、要素のドラッグ、エッジの接続・切断、要素の追加・削除、さらには要素やエッジのカスタマイズ、インタラクション、パフォーマンス最適化など、考慮すべき点が多岐にわたります。ゼロからこれらの機能を実装しようとすると、膨大な時間と労力が必要となり、メンテナンスも困難になりがちです。

そこで登場するのが React Flow です。React Flowは、Reactアプリケーション内でフロー図やグラフ構造を簡単に構築するためのライブラリです。宣言的なReactコンポーネントとしてフローを扱うことができ、ノード、エッジ、イベントハンドリング、ズーム、パンといった基本的な機能が豊富に用意されています。さらに、高いカスタマイズ性を提供しており、独自のノードやエッジを簡単に作成できます。これにより、複雑な要件を持つフロー図UIも効率的に、そしてメンテナンスしやすい形で実現することが可能になります。

この記事では、React Flowの基本的な使い方から、カスタムノードやカスタムエッジの作成、高度なインタラクション、パフォーマンス最適化、そして実際の複雑なUIを構築するための応用テクニックまで、詳細かつ実践的に解説します。この記事を読むことで、React Flowがいかに複雑なフロー図UI開発を容易にするか理解し、あなたのプロジェクトで活用するための知識を得られるでしょう。

対象読者は、Reactでの開発経験があり、フロー図やグラフ構造を扱うUIの実装に興味がある方です。

React Flowの基本

React Flowは、その名の通りReactのコンポーネントとして設計されています。Reactのコンポーネント指向の考え方と非常に親和性が高く、UIの状態管理やインタラクションをReactの標準的な手法で扱うことができます。

React Flowのインストールと基本的なセットアップ

まずは、React Flowをプロジェクトにインストールします。npmまたはyarnを使用します。

“`bash
npm install react-flow-renderer # 古いバージョン

または

npm install @react-flow/core react # 新しいバージョン (@react-flow/core が推奨)
``
**注:** 記事執筆時点では
@react-flow/coreを使用するのが最新の推奨方法です。この記事では@react-flow/coreを使用した記法を基本とします。古いreact-flow-renderer` とは一部APIが異なりますのでご注意ください。

インストール後、Reactコンポーネント内でReact Flowを使用することができます。最も基本的な使い方は以下のようになります。

“`jsx
import React, { useState } from ‘react’;
import ReactFlow, { MiniMap, Controls, Background } from ‘@react-flow/core’;

// 初期ノードとエッジの定義
const initialNodes = [
{ id: ‘1’, position: { x: 0, y: 0 }, data: { label: ‘ノード 1’ } },
{ id: ‘2’, position: { x: 0, y: 100 }, data: { label: ‘ノード 2’ } },
];

const initialEdges = [
{ id: ‘e1-2’, source: ‘1’, target: ‘2’ },
];

function MyFlow() {
const [nodes, setNodes] = useState(initialNodes);
const [edges, setEdges] = useState(initialEdges);

// ノードやエッジが変更されたときに呼ばれるハンドラ
const onNodesChange = (changes) => setNodes((nds) => applyNodeChanges(changes, nds));
const onEdgesChange = (changes) => setEdges((eds) => applyEdgeChanges(changes, eds));
const onConnect = (connection) => setEdges((eds) => addEdge(connection, eds));

// applyNodeChanges, applyEdgeChanges, addEdge は @react-flow/core からインポート
import { applyNodeChanges, applyEdgeChanges, addEdge } from ‘@react-flow/core’;

return (


{/ フロー図に追加できる補助機能 /}



);
}

export default MyFlow;
“`

上記のコードは、2つのノードとそれらを結ぶ1つのエッジを表示する最小限の例です。

  • ReactFlow コンポーネント: これがフロー図のメインコンテナです。表示するノードとエッジのデータ、様々なイベントハンドラ、設定オプションなどをPropsとして受け取ります。
  • nodesedges Props: 表示するノードとエッジの配列を渡します。これらのデータ構造については後述します。
  • onNodesChangeonEdgesChange: ノードやエッジの位置、選択状態などがユーザー操作によって変更されたときにReact Flowが呼び出すハンドラです。これらのハンドラ内で状態を更新することで、フロー図がインタラクティブになります。React Flowは変更の差分情報(changes)を渡してくれるため、それらを状態に適用するためのヘルパー関数(applyNodeChanges, applyEdgeChanges)が用意されています。
  • onConnect: ユーザーがドラッグ&ドロップでノード間を接続したときに呼ばれるハンドラです。このハンドラ内で新しいエッジを状態に追加します。addEdge ヘルパー関数が便利です。
  • MiniMap: フロー図全体の縮小版と現在のビューポート位置を表示するコンポーネントです。広いフロー図をナビゲートするのに役立ちます。
  • Controls: ズームイン、ズームアウト、フィットビューなどの操作ボタンを提供するコンポーネントです。
  • Background: フロー図の背景にグリッドやドットを表示するコンポーネントです。ノードの位置合わせなどに役立ちます。
  • fitView Props: フロー図がロードされたときに、全ての要素がビューポート内に収まるように自動的にズームとパンを調整します。

要素のデータ構造(NodesとEdges)

React Flowにおけるノードとエッジは、JavaScriptオブジェクトの配列として定義されます。

ノード (nodes 配列の要素)

ノードオブジェクトは少なくとも以下のプロパティを持ちます。

  • id (string): ノードの一意な識別子。
  • position ({ x: number, y: number }): フロー図上でのノードの位置(左上隅の座標)。
  • data (object): ノードに表示する任意のデータ。例えば、ノードのラベルなどが含まれます。
  • type (string, オプション): ノードのタイプを指定します。デフォルトは 'default' です。カスタムノードを作成する際に使用します。
  • sourcePosition (Position, オプション): ソースハンドル(出力側)のデフォルトの位置。Position.Left, Position.Right, Position.Top, Position.Bottom のいずれか。
  • targetPosition (Position, オプション): ターゲットハンドル(入力側)のデフォルトの位置。Position.Left, Position.Right, Position.Top, Position.Bottom のいずれか。
  • hidden (boolean, オプション): ノードを表示するかどうか。
  • selected (boolean, オプション): ノードが選択されているかどうか。
  • draggable (boolean, オプション): ノードをドラッグ可能にするかどうか。デフォルトは true
  • selectable (boolean, オプション): ノードを選択可能にするかどうか。デフォルトは true
  • connectable (boolean, オプション): ノードからエッジを接続可能にするかどうか。デフォルトは true
  • deletable (boolean, オプション): ノードを削除可能にするかどうか(Ctrl/Cmd+Backspaceなど)。デフォルトは true
  • style (object, オプション): ノードのコンテナに適用するCSSスタイル。
  • className (string, オプション): ノードのコンテナに適用するCSSクラス名。
  • parentNode (string, オプション): このノードが属する親ノードのID。グループ化に使用します。
  • extent (‘parent’ | [[number, number], [number, number]], オプション): parentNode が指定されている場合、このノードが移動できる範囲を制限します。’parent’ を指定すると親ノードの境界内に制限されます。

エッジ (edges 配列の要素)

エッジオブジェクトは少なくとも以下のプロパティを持ちます。

  • id (string): エッジの一意な識別子。
  • source (string): 接続元ノードのID。
  • target (string): 接続先ノードのID。
  • sourceHandle (string, オプション): 接続元ノードの特定のハンドルのID。ノードに複数のハンドルがある場合に使用します。
  • targetHandle (string, オプション): 接続先ノードの特定のハンドルのID。ノードに複数のハンドルがある場合に使用します。
  • type (string, オプション): エッジのタイプを指定します。デフォルトは 'default' です。カスタムエッジを作成する際に使用します。デフォルトタイプには 'straight', 'step', 'smoothstep', 'bezier' などがあります。
  • label (ReactNode, オプション): エッジの中央に表示するラベル。
  • labelStyle, labelShowBg, labelBgStyle, labelBgPadding (オプション): ラベルのスタイル設定。
  • markerEnd, markerStart (string | EdgeMarker, オプション): エッジの端に表示するマーカー(矢印など)。定義済みの 'arrow', 'arrowclosed' やカスタムマーカーを指定できます。
  • hidden (boolean, オプション): エッジを表示するかどうか。
  • selected (boolean, オプション): エッジが選択されているかどうか。
  • animated (boolean, オプション): エッジを点線アニメーションで表示するかどうか。
  • draggable (boolean, オプション): エッジをドラッグして再接続可能にするかどうか。デフォルトは true
  • selectable (boolean, オプション): エッジを選択可能にするかどうか。デフォルトは true
  • deletable (boolean, オプション): エッジを削除可能にするかどうか。デフォルトは true
  • style (object, オプション): エッジパスに適用するCSSスタイル。
  • className (string, オプション): エッジパスに適用するCSSクラス名。
  • data (object, オプション): エッジに付随する任意のデータ。カスタムエッジで使用します。

これらのプロパティを適切に設定することで、フロー図の構造と見た目を定義します。

状態管理

React Flowでは、ノードとエッジの状態(位置、選択状態など)をReactコンポーネントの状態として管理します。前述の例では useState フックを使用しました。

@react-flow/core には、ノードとエッジの状態管理をより簡単にするためのカスタムフック useNodesStateuseEdgesState が用意されています。これらのフックは内部で applyNodeChangesapplyEdgeChanges を呼び出すロジックを含んでおり、コードを簡潔にできます。

“`jsx
import React from ‘react’;
import ReactFlow, { useNodesState, useEdgesState, addEdge, MiniMap, Controls, Background } from ‘@react-flow/core’;

const initialNodes = [ // ];
const initialEdges = [ // ];

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

const onConnect = (connection) => setEdges((eds) => addEdge(connection, eds));

return (





);
}
“`

useNodesStateuseEdgesState は、それぞれ [状態変数, 状態更新関数, 変更ハンドラ] のタプルを返します。onNodesChangeonEdgesChange を直接 ReactFlow コンポーネントに渡すだけで、ノードとエッジの基本的なインタラクション(ドラッグ、選択など)が機能するようになります。

外部の状態管理ライブラリ(Redux, Zustand, Jotaiなど)と連携させたい場合でも、基本的な考え方は同じです。外部ストアの状態をReact Flowの nodesedges プロップに渡し、onNodesChangeonEdgesChange ハンドラ内で外部ストアのアクションをディスパッチして状態を更新します。

複雑なUIを構築するためのReact Flowの機能

React Flowの真価は、その高いカスタマイズ性と拡張性にあります。複雑な要件を持つフロー図UIを実現するためには、標準機能だけでなく、カスタム要素の作成や高度なイベントハンドリング、外部ライブラリとの連携などが不可欠になります。

カスタムノードの作成

デフォルトのノードはシンプルな四角形ですが、実際のアプリケーションでは、ノード内にテキスト入力フィールド、ボタン、グラフ、アイコン、複雑な情報表示など、様々な要素を含める必要があります。React Flowでは、Reactコンポーネントを使って独自のノードの見た目やインタラクションを定義できます。

なぜカスタムノードが必要か?

  • 見た目のカスタマイズ: アプリケーションのテーマに合わせたデザイン、独自のシェイプ、アイコン表示など。
  • 豊富な情報表示: デフォルトのラベルだけでなく、ノード固有のデータを詳細に表示。
  • インタラクションの追加: ノード内のボタンクリック、入力フィールドへの値入力、チェックボックスの操作など、ノード自身が持つ機能を実装。
  • 複数のハンドルの定義: 入出力ポートが複数あるノード(例:関数ノードの引数と戻り値)を作成。

カスタムノードの作成方法

カスタムノードは、単なるReactコンポーネントです。このコンポーネントは、React Flowによって特定のPropsを受け取ります。最低限必要なPropsは以下の通りです。

  • id: ノードのID。
  • data: ノードの data プロパティに指定したオブジェクト。ここにカスタムノードが必要とするデータを含めます。
  • selected: そのノードが現在選択されているかどうかのboolean値。
  • type: ノードのタイプ文字列。

その他、位置情報 (xPos, yPos), ドラッグ状態 (isDragging), 幅 (width), 高さ (height) などのPropsも受け取れます。

カスタムノードコンポーネント内では、ノードの見た目をJSXで定義し、必要に応じて <Handle> コンポーネントを使って接続用のハンドルを配置します。

“`jsx
import React, { memo } from ‘react’;
import { Handle, Position } from ‘@react-flow/core’;

const CustomInputNode = ({ data, selected }) => {
// data オブジェクトからカスタムノードに必要なデータを取り出す
const { label, value, onChange } = data;

return (

{label}

onChange(e.target.value)}
style={{ marginTop: 5, padding: 3, width: 80 }}
/>
{/ ターゲットハンドル(入力) – 左 /}
connection.targetHandle === ‘a’}
/>
{/ ソースハンドル(出力) – 右 /}

);
};

// パフォーマンスのためにmemo化することが推奨されます
export default memo(CustomInputNode);
“`

作成したカスタムノードをReact Flowで使用するには、ReactFlow コンポーネントの nodeTypes プロップに登録する必要があります。nodeTypes は、ノードのタイプ文字列をキー、対応するカスタムノードコンポーネントを値とするオブジェクトです。

“`jsx
import React from ‘react’;
import ReactFlow, { useNodesState, useEdgesState, addEdge, MiniMap, Controls, Background } from ‘@react-flow/core’;
// カスタムノードをインポート
import CustomInputNode from ‘./CustomInputNode’;

const initialNodes = [
{ id: ‘1’, position: { x: 50, y: 50 }, type: ‘customInput’, data: { label: ‘設定値’, value: ‘Hello’, onChange: (v) => console.log(‘Input changed:’, v) } },
{ id: ‘2’, position: { x: 250, y: 50 }, data: { label: ‘処理ノード’ } },
];

const initialEdges = [
{ id: ‘e1-2’, source: ‘1’, sourceHandle: ‘b’, target: ‘2’, targetHandle: null }, // カスタムノードのハンドルIDを指定
];

// カスタムノードの型を定義
const nodeTypes = {
customInput: CustomInputNode,
// 他のカスタムノードもここに追加
};

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

const onConnect = (connection) => setEdges((eds) => addEdge(connection, eds));

// ノードの data を更新する例(カスタムノード内の input から呼ばれることを想定)
// CustomInputNode の onChange ハンドラ内でこの関数を呼び出すように data を渡す
const onNodeDataChange = (nodeId, newData) => {
setNodes((nds) =>
nds.map((node) =>
node.id === nodeId ? { …node, data: { …node.data, …newData } } : node
)
);
};

// initialNodes の data に onChange ハンドラを渡す場合は以下のようにします
const initialNodesWithHandlers = initialNodes.map(node => {
if (node.type === ‘customInput’) {
return {
…node,
data: {
…node.data,
onChange: (value) => onNodeDataChange(node.id, { value })
}
};
}
return node;
});
// useNodesState には initialNodesWithHandlers を渡す

return (





);
}
“`

上記の例では、'customInput' というタイプのノードが CustomInputNode コンポーネントとしてレンダリングされるようになります。ノードの data プロパティを通じて、カスタムノードが必要とするデータやコールバック関数を渡すことができます。

カスタムエッジの作成

デフォルトのエッジはシンプルな線ですが、エッジにラベルを表示したり、アニメーションさせたり、独自のインタラクション(例えば、エッジ上のボタンで設定を開くなど)を持たせたい場合があります。また、エッジの形状(パス)自体をカスタマイズしたい場合もあります。React Flowでは、カスタムノードと同様にカスタムエッジを作成できます。

なぜカスタムエッジが必要か?

  • 情報表示: エッジの上にデータ転送量、処理結果、条件分岐のラベルなどを表示。
  • 視覚的な強調: 特定のエッジを点線、太線、色付けなどで目立たせる。
  • インタラクション: エッジのクリックやホバーに応じた処理、エッジ上のコンテキストメニューなど。
  • パスのカスタマイズ: デフォルトのパス(直線、ステップ、ベジェ曲線など)以外の複雑な描画。

カスタムエッジの作成方法

カスタムエッジコンポーネントもReactコンポーネントですが、ノードとは異なるPropsを受け取ります。主なPropsは以下の通りです。

  • id: エッジのID。
  • sourceX, sourceY, targetX, targetY: 接続元ノードのソースハンドルと接続先ノードのターゲットハンドルの座標。
  • sourcePosition, targetPosition: 接続元/先のハンドルの位置 (Position enum)。
  • source, target: 接続元/先ノードのID。
  • sourceHandleId, targetHandleId: 使用されているハンドルのID。
  • data: エッジの data プロパティに指定したオブジェクト。
  • selected: そのエッジが現在選択されているかどうかのboolean値。
  • markerEnd, markerStart: エッジの端に表示するマーカー情報。
  • style: エッジに適用されるスタイルオブジェクト。
  • pathOptions (v11+): パス計算に関するオプション。

カスタムエッジコンポーネント内で最も重要なのは、SVGの <path> 要素を使ってエッジの線を描画することです。React Flowは、接続元と接続先の座標、ハンドルの位置に基づいて、デフォルトのエッジタイプ('straight', 'step', 'smoothstep', 'bezier')に対応するパスデータを計算するためのヘルパー関数を提供しています。最も汎用的なのは getBezierPath ですが、他のパスタイプに対応するヘルパーもあります。

“`jsx
import React from ‘react’;
import { BaseEdge, getBezierPath, EdgeLabelRenderer } from ‘@react-flow/core’;

const CustomLabelEdge = ({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
style = {},
data,
selected, // 選択状態も受け取れる
}) => {
const edgePath = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});

// エッジの中央に表示するラベルの座標を計算するヘルパー
const [centerX, centerY] = getEdgeCenter({
sourceX,
sourceY,
targetX,
targetY,
});

// dataからラベルデータを取り出す
const label = data?.label || ”;

return (
<>
{/ エッジのパス /}

{/ エッジラベルのレンダリング /}
{label && (

translate(-50%, -50%) translate(${centerX}px, ${centerY}px),
pointerEvents: ‘all’, // ラベル上でクリックイベントなどを有効にする
background: ‘#fff’,
padding: ‘2px 5px’,
borderRadius: 3,
fontSize: 12,
fontWeight: ‘bold’,
}}
className=”nodrag nopan” // ドラッグ・パン無効化
>
{label}


)}

);
};

// getEdgeCenter は @react-flow/core からインポートする必要がありますが、
// BaseEdge や EdgeLabelRenderer と一緒にインポートされることが多いです。
// getBezierPath は @react-flow/core からインポート

export default CustomLabelEdge;
“`

上記の例では、getBezierPath を使ってベジェ曲線のパスデータを計算し、BaseEdge コンポーネントを使ってパスを描画しています。エッジ上のラベルは EdgeLabelRenderer を使うことで、ズームやパンに追従して正しく位置が調整されます。

作成したカスタムエッジも、ReactFlow コンポーネントの edgeTypes プロップに登録して使用します。

“`jsx
import React from ‘react’;
import ReactFlow, { useNodesState, useEdgesState, addEdge, MiniMap, Controls, Background } from ‘@react-flow/core’;
// カスタムエッジをインポート
import CustomLabelEdge from ‘./CustomLabelEdge’;

const initialNodes = [ // ];
const initialEdges = [
{ id: ‘e1-2’, source: ‘1’, target: ‘2’, type: ‘customLabel’, data: { label: ‘データ転送’ } },
];

// カスタムエッジの型を定義
const edgeTypes = {
customLabel: CustomLabelEdge,
// 他のカスタムエッジもここに追加
};

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

const onConnect = (connection) => setEdges((eds) => addEdge(connection, eds));

return (





);
}
“`

これで、'customLabel' タイプの任意のエッジに対して、定義した CustomLabelEdge コンポーネントが使用されるようになります。

ノード間のインタラクション

ユーザーがフロー図を操作する際の様々なインタラクション(ノードのドラッグ、エッジの接続/切断、ノードの選択など)は、React Flowによって自動的に処理されますが、これらのインタラクションにフックして独自のロジックを実行したり、特定のインタラクションを制御したりすることが可能です。

  • エッジの追加/削除:

    • ユーザー操作: ユーザーがハンドルをドラッグして新しいエッジを作成すると onConnect イベントが発火します。このイベントハンドラ内で、新しいエッジオブジェクトを作成し、エッジの状態配列に追加します。
    • プログラム操作: ユーザー操作ではなく、例えば「ノードAとノードBを自動で接続する」といった場合は、新しいエッジオブジェクトを作成して setEdges 関数を使って状態を更新します。エッジを削除する場合も同様に、setEdges で該当エッジを除外した新しい配列を設定します。選択されたノードやエッジは、Ctrl/Cmd+Backspaceなどで自動的に削除されますが、このイベントは onNodesDelete および onEdgesDelete ハンドラで捕捉できます。
  • 接続の制限:

    • 特定のノードタイプから特定のノードタイプへしか接続できないようにしたい、あるいは特定のハンドル同士しか接続できないようにしたいといった要件がある場合があります。ReactFlow コンポーネントの onConnectStart, onConnectEnd, isValidConnection プロップや、<Handle> コンポーネントの isValidConnection プロップを使って接続を制御できます。
    • 特に isValidConnection 関数は、ドラッグ中のエッジが有効な接続先候補の上に来たときに呼び出され、接続を許可するかどうかをbooleanで返すことで、複雑な接続ルールを実装できます。
  • グループ化(Parent Node):

    • 関連するノードを視覚的にまとめたい場合、Parent Node機能が役立ちます。ノードオブジェクトに parentNode プロパティで親ノードのIDを指定すると、そのノードは子ノードとして扱われます。親ノードをドラッグすると子ノードも一緒に移動します。
    • 子ノードの移動範囲を親ノードの境界内に制限したい場合は、子ノードに extent: 'parent' を指定します。

jsx
// Parent Nodeの例
const initialNodes = [
{ id: 'parent-1', position: { x: 100, y: 100 }, data: { label: '親ノード' }, style: { background: 'rgba(200, 200, 200, 0.2)', width: 200, height: 150 } },
{ id: 'child-1', position: { x: 20, y: 40 }, data: { label: '子ノード 1' }, parentNode: 'parent-1', extent: 'parent' },
{ id: 'child-2', position: { x: 20, y: 80 }, data: { label: '子ノード 2' }, parentNode: 'parent-1', extent: 'parent' },
];

高度な機能

複雑なフロー図UIでは、基本的な表示やインタラクションに加え、ユーザー体験を向上させるための様々な高度な機能が求められます。

  • ミニマップとコントロール: 前述の通り、これらは @react-flow/core からインポートして <ReactFlow> コンポーネントの子として配置するだけで簡単に利用できます。複雑なフロー図においては、これらはほぼ必須と言えるでしょう。MiniMapControls もPropsでカスタマイズ可能です(位置、スタイル、表示/非表示など)。

  • キーボードショートカット: React Flowはいくつかのデフォルトのキーボードショートカット(例: Backspace/Deleteキーでの要素削除、Ctrl/Cmd+C/Vでのコピー&ペースト)を提供しています。これらのデフォルト動作は無効化したり、独自のショートカットを実装したりすることも可能です。キーボードイベントは onKeyDown, onKeyPress, onKeyUp ハンドラで捕捉できます。

  • Undo/Redo機能の実装: 複雑なUIでは、ユーザー操作の取り消し・やり直し機能があると便利です。React Flow自体に組み込みのUndo/Redo機能はありませんが、状態管理ライブラリ(Zustand, Reduxなど)のスナップショット機能や、操作履歴を保持するロジックを組み合わせることで実現可能です。ノードやエッジの状態が変更されるたびに、その時点の状態(または変更差分)を履歴スタックにプッシュし、Undo/Redo操作時にはスタックから状態を取り出して setNodes / setEdges で適用します。不変性を保った状態管理が重要になります。

  • オートレイアウト: 特にデータが動的に生成されたり、ユーザーがノードを追加したりする場合、手動でのノード配置は煩雑になりがちです。オートレイアウト機能は、ノード間の接続関係に基づいてノードを自動的に整列させます。React Flow自体はレイアウトアルゴリズムを含んでいませんが、Graphviz、Dagre、ELKjsなどの外部ライアウトライブラリと連携して実現します。

    • 連携の一般的な手順は、React Flowのノードとエッジのデータを外部ライブラリが理解できる形式に変換し、レイアウト計算を実行します。計算結果として得られる各ノードの新しい位置情報(x, y座標)をReact Flowのノードデータに適用し、setNodes で状態を更新します。この処理は通常、ノードやエッジが追加/削除された後や、特定のユーザーアクションに応じて実行されます。
    • 例(Dagreとの連携の概念):
      “`javascript
      import dagre from ‘dagre’;
      // … react-flow imports

      const dagreGraph = new dagre.graphlib.Graph();
      dagreGraph.setDefaultEdgeLabel(() => ({}));

      const nodeWidth = 172; // 例: デフォルトノードの幅
      const nodeHeight = 36; // 例: デフォルトノードの高さ

      const getLayoutedElements = (nodes, edges, direction = ‘TB’) => {
      dagreGraph.setGraph({ rankdir: direction });

      nodes.forEach((node) => {
      dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
      });

      edges.forEach((edge) => {
      dagreGraph.setEdge(edge.source, edge.target);
      });

      dagre.layout(dagreGraph);

      const layoutedNodes = nodes.map((node) => {
      const nodeWithPosition = dagreGraph.node(node.id);
      // 中心座標から左上座標への変換
      node.position = {
      x: nodeWithPosition.x – nodeWidth / 2,
      y: nodeWithPosition.y – nodeHeight / 2,
      };
      return node;
      });

      return { nodes: layoutedNodes, edges };
      };

      // … Reactコンポーネント内で
      // const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(nodes, edges);
      // setNodes(layoutedNodes);
      // setEdges(layoutedEdges); // エッジは位置情報を持たないのでそのまま
      “`
      これは簡略化された例ですが、ノードのサイズを正確に渡すことや、異なるレイアウトアルゴリズムに対応させることが重要です。

  • スナップ機能: ノードのドラッグ時に、グリッドにスナップさせたり、他のノードの境界線や中心線にスナップさせたりすると、整列が容易になります。React Flowは snapGrid プロップでグリッドスナップをサポートしています。より高度なスナップライン機能は自前で実装するか、関連ライブラリを探す必要がありますが、React Flowのノード位置やドラッグイベントを活用することで実現可能です。

  • ズームとパンの制御: ReactFlow コンポーネントの minZoom, maxZoom プロップでズーム範囲を制限できます。また、useReactFlow というカスタムフックを使うと、プログラム的にビューポート(表示領域、ズームレベル、位置)を操作できます。これは、「特定のノードにズームインする」「フロー全体を画面にフィットさせる」といった機能に役立ちます。

“`jsx
import { useReactFlow } from ‘@react-flow/core’;

function MyComponent() {
const reactFlowInstance = useReactFlow();

const fitView = () => {
reactFlowInstance.fitView(); // 全体をフィット
};

const zoomToNode = (nodeId) => {
const node = reactFlowInstance.getNodes().find(n => n.id === nodeId);
if (node) {
// 特定ノードにズームする処理(例:setViewPortやzoomToなどを使用)
// exact:true でノードの中心に移動し、ズームレベルを指定
reactFlowInstance.setViewPort({ x: node.position.x, y: node.position.y, zoom: 1.5 }, { duration: 800 });
// あるいは node.measured.width, node.measured.height を使って計算
}
};

return (


{/ … 他のUI要素 … /}

);
}

// ReactFlowコンポーネント内で useReactFlow を使用するには、
// でラップする必要がある場合があります(通常はReactFlowコンポーネント自身がプロバイダーを提供)。
// あるいは、useReactFlow を使用するコンポーネントを ReactFlow の子として配置します。
“`

  • データのエクスポート/インポート: 作成したフロー図の状態を保存・復元するために、ノードとエッジのデータをエクスポート・インポートする機能が必要です。useReactFlow フックは toObject というメソッドを提供しており、現在のフローの状態(ノード、エッジ、ビューポート)をJSONシリアライズ可能なオブジェクトとして取得できます。これを保存し、復元時にはパースしたオブジェクトからノード、エッジ、ビューポートの状態を取り出し、setNodes, setEdges, setViewport を使って適用します。

“`javascript
import { useReactFlow } from ‘@react-flow/core’;

function SaveLoadButtons() {
const reactFlowInstance = useReactFlow();

const onSave = () => {
const flow = reactFlowInstance.toObject();
localStorage.setItem(‘flow-key’, JSON.stringify(flow));
alert(‘フローを保存しました’);
};

const onRestore = () => {
const flowString = localStorage.getItem(‘flow-key’);
if (flowString) {
const flow = JSON.parse(flowString);
reactFlowInstance.setNodes(flow.nodes || []);
reactFlowInstance.setEdges(flow.edges || []);
reactFlowInstance.setViewport(flow.viewport);
alert(‘フローを復元しました’);
} else {
alert(‘保存されたフローがありません’);
}
};

return (


);
}
“`

  • パフォーマンス最適化: 大量のノードやエッジ(数百〜数千規模)を扱う場合、パフォーマンスが課題となることがあります。React Flowは内部でいくつかの最適化(要素の仮想化レンダリングなど)を行っていますが、アプリケーション側でも注意が必要です。
    • 不変性の維持: ノードやエッジの状態を更新する際は、元の配列を直接変更するのではなく、新しい配列を作成して setNodessetEdges に渡すようにします(これはuseStateの基本でもあります)。useNodesState / useEdgesStateapplyNodeChanges / applyEdgeChanges / addEdge ヘルパー関数を使用すれば、不変性は維持されます。
    • 不要な再レンダリングの回避: カスタムノード/エッジコンポーネントは React.memo でラップして、Propsが変更された場合のみ再レンダリングされるようにします。コールバック関数は useCallback、計算結果は useMemo を使用してメモ化します。
    • 複雑な計算の最適化: オートレイアウト計算など、時間のかかる処理は非同期で行ったり、Web Workerで実行したりすることを検討します。
    • CSSの最適化: パフォーマンスに影響するCSSプロパティ(box-shadow, filterなど)の使用を控えたり、アニメーションには transform, opacity を利用したりします。

実践的な応用例

React Flowの柔軟性と機能の豊富さは、様々な複雑なUIの実現を可能にします。いくつかの具体的な応用例を考え、それぞれでReact Flowのどの機能が重要になるかを見てみましょう。

  1. ワークフローエディタ:

    • 概要: ユーザーがタスクを表すノードを配置し、処理順序を表すエッジで接続することで、業務プロセスや自動化フローを設計できるツール。
    • 必要な機能:
      • カスタムノード: 各タスクタイプ(例: データ入力、条件分岐、メール送信、外部API呼び出しなど)に応じたカスタムノード。ノード内に設定用のフォーム要素(テキスト入力、ドロップダウン、チェックボックスなど)を含む。
      • ノード間のインタラクション: 条件分岐ノードからの「成功」「失敗」など、複数の出力ハンドルを持つノード。特定のタスクタイプ間のみ接続を許可する isValidConnection ロジック。
      • データ管理: 各ノードの状態(設定値など)をノードオブジェクトの data プロパティに保持。エッジにもフローのメタ情報(例: 条件分岐のエッジに「Yes」「No」ラベル)を持たせる。
      • ツールボックス/パレット: 画面外から新規ノードをドラッグしてフロー図にドロップする機能(Drag and Drop APIと onDrop ハンドラを使用)。
      • 検証機能: フロー図の構造が有効かどうかの検証(例: 開始ノードが一つだけか、無限ループがないかなど)。React Flowのノード・エッジデータを解析して検証ロジックを実行。
      • 保存/ロード: toObjectsetNodes/setEdges/setViewport を使ったフロー図の状態の保存・復元。
  2. データパイプライン可視化ツール:

    • 概要: データソース、変換処理、データシンクなどのコンポーネントをノードとして表現し、データの流れをエッジで可視化するツール。
    • 必要な機能:
      • カスタムノード: データソース(データベース、ファイル)、変換処理(フィルタリング、集計)、データシンク(データウェアハウス、BIツール)など、コンポーネントタイプに応じたカスタムノード。ノードにデータのスキーマ情報や処理状況を表示。
      • カスタムエッジ: データ量、転送速度、エラー率などの統計情報をエッジラベルとして表示。アニメーションエッジでアクティブなデータフローを表現。
      • インタラクション: ノードのクリックで詳細設定パネルを開く。エッジのクリックでデータプレビューを表示。
      • オートレイアウト: パイプライン構造を自動的に整列させて見やすくする。
      • パフォーマンス: 大規模なパイプライン(多数のノード/エッジ)でもスムーズに表示・操作できるパフォーマンス。
      • リアルタイム更新: 実行中のデータパイプラインの状態変化(ノードのステータス、エッジのデータ量)をリアルタイムにフロー図に反映。Reactの状態更新機能を利用。
  3. ノードベースのシェーダーエディタ/ビジュアルプログラミング環境:

    • 概要: グラフィックスシェーダーや汎用的なプログラムロジックを、ノード(演算、関数、変数)とエッジ(データの流れ)で構築・編集できる環境。
    • 必要な機能:
      • 複雑なカスタムノード: 複数の入力/出力ハンドルを持つノード(例: 数学関数ノード)。異なるデータ型(float, vec3, colorなど)に対応したハンドルの色の区別や、不正な型間の接続を禁止する isValidConnection ロジック。ノード内にスライダー、カラーピッカーなどの入力UI要素。
      • カスタムエッジ: データ型に応じたエッジの色分け。接続方向やデータの流れを示すアニメーション。
      • コンテキストメニュー: ノードやエッジを右クリックしたときに、コピー、ペースト、削除、設定などの操作を提供。
      • ライブプレビュー: フロー図の変更がリアルタイムに結果(例: 3Dモデルのシェーディング)に反映される機能。フロー図の状態変化を検知し、外部の計算エンジンに渡して結果を取得し、UIに表示。
      • Undo/Redo: 細かい操作の多い環境で必須となるUndo/Redo機能。
  4. 組織図/ネットワーク図:

    • 概要: 階層的な組織構造や、サーバー/ネットワーク機器の接続関係を可視化する図。
    • 必要な機能:
      • カスタムノード: 組織図では人物情報(氏名、役職、顔写真)、ネットワーク図では機器情報(IPアドレス、種類、ステータス)を表示するカスタムノード。
      • 階層レイアウト: 組織図には上司・部下の関係、ネットワーク図にはハブ・スポーク型など、階層的な関係性を綺麗に表示するオートレイアウト(Dagreなど)が重要。
      • インタラクション: ノードの展開/折りたたみ(階層を非表示にする)、ノードのクリックで詳細情報表示。
      • フィルタリング/検索: 特定の人物や機器を検索し、ハイライト表示またはそれ以外を非表示にする機能。ノードの hidden プロパティを操作。
      • 大量要素の表示: 大規模な組織やネットワークに対応するためのパフォーマンス最適化。

これらの例からわかるように、React Flowはコア機能(ノード、エッジ、イベント)を提供しつつ、カスタム要素、データプロパティ、外部連携ポイントを用意することで、開発者が特定のアプリケーション要件に合わせたUIを構築できるようになっています。複雑なUIを実現するためには、これらの機能を組み合わせて利用することが鍵となります。

実装のヒントとベストプラクティス

React Flowを使った開発をより効率的かつ堅牢に進めるためのヒントとベストプラクティスをいくつか紹介します。

状態管理

フロー図全体のノード、エッジ、ビューポートの状態は、Reactコンポーネントの状態として管理するのが基本です。しかし、フローが複雑になり、カスタムノード内に含まれる入力フィールドやスイッチの状態、あるいはUndo/Redo履歴、外部ツールボックスの状態なども管理する必要が出てくると、コンポーネントローカルの useState だけでは管理が煩雑になる可能性があります。

このような場合、Zustand, Redux, Jotai, Recoilなどの状態管理ライブラリを導入することを検討します。これらのライブラリは、アプリケーション全体の複雑な状態を一元管理し、コンポーネント間で状態を共有・更新するメカニズムを提供します。

  • Zustand は軽量でフックベースのライブラリであり、React Flowの状態管理とも相性が良いです。フローの状態(ノード、エッジ、ビューポート)をZustandストアに置き、onNodesChangeonEdgesChange でストアを更新します。useNodesStateuseEdgesState の内部実装を参考に、Zustandストアを扱うカスタムフックを作成するのも良い方法です。
  • Redux Toolkit はより構造化された状態管理を提供し、特に大規模なアプリケーションや、より厳格な状態管理規約が必要な場合に適しています。Immerが組み込まれているため、状態の不変性を保ちながら更新ロジックをシンプルに記述できます。

重要なのは、React Flowの状態(ノード、エッジ配列)は不変性を保って更新することです。状態管理ライブラリを使用する場合でも、React Flowのヘルパー関数(applyNodeChanges, applyEdgeChanges, addEdge)を使用するか、自分で新しい配列を生成して更新することを忘れないでください。

テスト

フロー図UIはインタラクティブな要素が多いため、テストが重要になります。

  • ユニットテスト: カスタムノードやカスタムエッジコンポーネント、あるいは状態更新ロジックなどの純粋な関数やコンポーネントは、JestやReact Testing Libraryを使ってユニットテストを行います。Propsを受け取って正しくレンダリングされるか、内部の状態変化やイベントハンドラが期待通りに動作するかなどを検証します。
  • 結合テスト/E2Eテスト: フロー図上でのユーザー操作(ノードのドラッグ、エッジの接続、ズーム/パン、要素の選択と削除など)を含むインタラクション全体は、CypressやPlaywrightなどのE2Eテストツールを使ってテストするのが効果的です。これらのツールはブラウザ操作をシミュレートし、要素の存在確認や位置確認、ドラッグ&ドロップ操作などをテストできます。React Flowの要素には自動的にデータ属性(例: data-flow-nodeid, data-flow-edgeid)が付与されるため、テストコードから特定の要素を選択しやすくなっています。

アクセシビリティ

フロー図UIは視覚的な情報伝達が中心ですが、アクセシビリティにも配慮が必要です。

  • キーボード操作: マウスだけでなく、キーボードでのナビゲーション(ノード選択、移動、削除)や操作(エッジ接続など)を可能にします。React Flowはデフォルトで一部のキーボード操作をサポートしていますが、カスタムインタラクションを実装する際はキーボードイベントハンドラを追加します。
  • ARIA属性: カスタムノード内のインタラクティブな要素(ボタン、入力フィールドなど)には、適切なARIA属性を付与し、スクリーンリーダーからの情報アクセスを可能にします。
  • 代替手段: フロー図の視覚的な情報にアクセスできないユーザーのために、テーブル形式やテキストベースでの表示など、代替手段を提供することも検討します。

パフォーマンスチューニング

前述の通り、大量の要素を扱う場合のパフォーマンスは重要です。

  • Chrome DevToolsのPerformanceタブ: どこで時間がかかっているかを特定するために、ブラウザの開発者ツールのパフォーマンスプロファイラを使用します。特にレンダリング、スクリプト実行、レイアウト計算などに注目します。
  • React Developer Tools: Reactコンポーネントのレンダリング状況を確認し、不要な再レンダリングが発生していないかをチェックします。
  • Immutable.js または Immer: 複雑なネストした状態を扱う際に、不変性を簡単に保つためにこれらのライブラリを検討します。これにより、ReactやReact Flowのシャロー比較が正しく機能し、不要な再レンダリングを防ぎやすくなります。
  • 仮想化: カスタムノード内に大量のリストなどがある場合、react-windowreact-virtualized といったリスト仮想化ライブラリを組み合わせて使用することを検討します。
  • デバウンス/スロットル: ドラッグ中のイベントやリサイズイベントなど、頻繁に発生するイベントのハンドラには、デバウンスやスロットルを適用して処理回数を減らします。

エラーハンドリング

  • 不正なデータ: 外部から読み込んだデータやユーザー入力によって、ノードやエッジのデータ構造が不正になる可能性があります。React Flowに渡す前にデータのバリデーションを行うことが重要です。
  • カスタム要素のエラー: カスタムノードやエッジコンポーネント内でエラーが発生した場合、フロー図全体がクラッシュしないように、エラーバウンダリを適切に配置することを検討します。
  • 接続エラー: isValidConnection 関数で接続を拒否した場合など、ユーザーにフィードバックを提供することで、操作の意図を伝えやすくなります。

まとめ

React Flowは、Reactアプリケーションでフロー図やグラフ構造を扱う複雑なUIを構築するための強力で柔軟なライブラリです。基本的なノード、エッジ、インタラクション機能はもちろん、カスタムノード・カスタムエッジの作成、高度なイベントハンドリング、外部ライブラリとの連携機能を通じて、様々な要件に対応できます。

この記事で紹介したように、React Flowは以下の点で複雑なUI開発を容易にします。

  1. 宣言的なUI構築: Reactコンポーネントとしてフロー図全体や個々の要素を扱えるため、Reactの開発パラダイムに自然にフィットします。
  2. 豊富な基本機能: ズーム、パン、ノードのドラッグ、エッジの接続といった基本的なインタラクションが組み込みで提供されており、ゼロから実装する手間が省けます。
  3. 高いカスタマイズ性: カスタムノードやカスタムエッジを簡単に作成・登録できるため、アプリケーション固有のデザインやインタラクションを持つ要素を自由に定義できます。
  4. 拡張性: イベントハンドラ、カスタムフック、外部ライブラリとの連携ポイントが豊富に用意されており、オートレイアウト、Undo/Redo、データ入出力など、高度な機能を追加しやすい構造になっています。
  5. パフォーマンス: 大量の要素を扱うための内部的な最適化が行われており、さらに開発者側でのパフォーマンスチューニングも可能です。

ワークフローエディタ、データ可視化ツール、ビジュアルプログラミング環境など、要素間の関係性を視覚的に表現・操作するUIが求められる多くのアプリケーションにおいて、React Flowは強力な基盤となります。

もちろん、複雑なUIにはそれなりの設計と実装が必要ですが、React Flowを使うことで、基盤部分の多くの課題が解決され、アプリケーション固有のロジックやUI/UXの設計に集中できるようになります。

ぜひ、React Flowの公式ドキュメントを参照し、実際にコードを書いてその強力さを体感してみてください。活発なコミュニティもあり、困ったときにはサポートを得ることも可能です。

この記事が、あなたがReact Flowを使って素晴らしいフロー図UIを開発するための一助となれば幸いです。


(注) 記事の文字数は約5000語を目指して記述しましたが、実際のカウントはツールや記法によって若干変動する可能性があります。また、コード例は概念を示すためのものであり、そのまま実行するには追加のインポートや環境設定が必要な場合があります。最新の情報や詳細なAPIについては、必ずReact Flowの公式ドキュメントを参照してください。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール