React Flow
作成日:
React ダイアグラム 可視化 フロントエンド TypeScript
React Flowは、Reactアプリケーションでノードベースのエディタやインタラクティブなダイアグラムを構築するためのライブラリです。ワークフローエディタ、マインドマップ、データパイプライン設計ツールなどの構築に適しています。
概要
React Flowとは
React Flowは、2019年にwebkidによって作成されたオープンソースのReactライブラリです。ノードとエッジ(接続線)で構成されるインタラクティブなグラフを簡単に構築できます。
特徴
- Reactネイティブ: Reactコンポーネントとしてノードを定義
- TypeScript対応: 型安全な開発
- 高パフォーマンス: 仮想化、メモ化による最適化
- 豊富な機能: ミニマップ、コントロール、背景パターン
- カスタマイズ性: 完全にカスタマイズ可能なノードとエッジ
- アクティブな開発: 定期的なアップデートと活発なコミュニティ
主なユースケース
- ワークフローエディタ
- マインドマップ
- データパイプライン設計ツール
- 状態機械エディタ
- ノードベースのプログラミングUI
- 組織図エディタ
基本的な使い方
インストール
npm install reactflow
基本的なセットアップ
import { useCallback } from 'react';
import ReactFlow, {
MiniMap,
Controls,
Background,
useNodesState,
useEdgesState,
addEdge,
Node,
Edge,
Connection,
} from 'reactflow';
import 'reactflow/dist/style.css';
// 初期ノードの定義
const initialNodes: Node[] = [
{
id: '1',
type: 'input',
data: { label: '入力ノード' },
position: { x: 250, y: 0 },
},
{
id: '2',
data: { label: '処理ノード' },
position: { x: 250, y: 100 },
},
{
id: '3',
type: 'output',
data: { label: '出力ノード' },
position: { x: 250, y: 200 },
},
];
// 初期エッジの定義
const initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2' },
{ id: 'e2-3', source: '2', target: '3' },
];
function Flow() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback(
(connection: Connection) => setEdges((eds) => addEdge(connection, eds)),
[setEdges]
);
return (
<div style={{ width: '100%', height: '500px' }}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
fitView
>
<Controls />
<MiniMap />
<Background variant="dots" gap={12} size={1} />
</ReactFlow>
</div>
);
}
export default Flow;
コア概念
ノード(Nodes)
ノードはグラフの基本要素です。各ノードには一意のID、位置、データが必要です。
const node: Node = {
id: 'unique-id',
type: 'default', // 'input', 'output', 'default', またはカスタムタイプ
position: { x: 100, y: 100 },
data: { label: 'ノードのラベル' },
// オプション
style: { background: '#eee', border: '1px solid #ddd' },
className: 'custom-node',
draggable: true,
selectable: true,
connectable: true,
};
組み込みノードタイプ
- input: 入力ノード(下部にハンドルのみ)
- output: 出力ノード(上部にハンドルのみ)
- default: デフォルトノード(上下にハンドル)
エッジ(Edges)
エッジはノード間の接続を表します。
const edge: Edge = {
id: 'edge-1',
source: 'node-1', // 始点ノードのID
target: 'node-2', // 終点ノードのID
// オプション
sourceHandle: 'handle-a', // 特定のハンドルを指定
targetHandle: 'handle-b',
type: 'default', // 'default', 'straight', 'step', 'smoothstep', またはカスタム
animated: true, // アニメーション付き
label: '接続ラベル',
style: { stroke: '#f00' },
};
組み込みエッジタイプ
- default / bezier: ベジェ曲線
- straight: 直線
- step: 階段状
- smoothstep: 滑らかな階段状
ハンドル(Handles)
ハンドルはノード上の接続ポイントです。
import { Handle, Position } from 'reactflow';
function CustomNode({ data }) {
return (
<div className="custom-node">
<Handle type="target" position={Position.Top} />
<div>{data.label}</div>
<Handle type="source" position={Position.Bottom} />
</div>
);
}
カスタムノードの作成
基本的なカスタムノード
import { Handle, Position, NodeProps } from 'reactflow';
type CustomNodeData = {
label: string;
description?: string;
};
function CustomNode({ data, isConnectable }: NodeProps<CustomNodeData>) {
return (
<div className="custom-node">
<Handle
type="target"
position={Position.Top}
isConnectable={isConnectable}
/>
<div className="custom-node-header">
{data.label}
</div>
{data.description && (
<div className="custom-node-body">
{data.description}
</div>
)}
<Handle
type="source"
position={Position.Bottom}
isConnectable={isConnectable}
/>
</div>
);
}
export default CustomNode;
カスタムノードの登録
import CustomNode from './CustomNode';
const nodeTypes = {
custom: CustomNode,
};
function Flow() {
return (
<ReactFlow
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
// ...
/>
);
}
カスタムノードの使用
const nodes: Node[] = [
{
id: '1',
type: 'custom', // カスタムタイプを指定
data: {
label: 'カスタムノード',
description: '説明文をここに',
},
position: { x: 100, y: 100 },
},
];
カスタムエッジの作成
基本的なカスタムエッジ
import { EdgeProps, getBezierPath } from 'reactflow';
function CustomEdge({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
style = {},
markerEnd,
data,
}: EdgeProps) {
const [edgePath] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
return (
<>
<path
id={id}
style={style}
className="react-flow__edge-path"
d={edgePath}
markerEnd={markerEnd}
/>
{data?.label && (
<text>
<textPath
href={`#${id}`}
style={{ fontSize: 12 }}
startOffset="50%"
textAnchor="middle"
>
{data.label}
</textPath>
</text>
)}
</>
);
}
カスタムエッジの登録
const edgeTypes = {
custom: CustomEdge,
};
function Flow() {
return (
<ReactFlow
edges={edges}
edgeTypes={edgeTypes}
// ...
/>
);
}
状態管理
useNodesState / useEdgesState
React Flowが提供するカスタムフックで、ノードとエッジの状態を管理します。
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
ノードの追加
const addNode = () => {
const newNode: Node = {
id: `node-${Date.now()}`,
data: { label: '新しいノード' },
position: { x: Math.random() * 400, y: Math.random() * 400 },
};
setNodes((nds) => [...nds, newNode]);
};
ノードの削除
const deleteNode = (nodeId: string) => {
setNodes((nds) => nds.filter((node) => node.id !== nodeId));
setEdges((eds) => eds.filter(
(edge) => edge.source !== nodeId && edge.target !== nodeId
));
};
ノードデータの更新
const updateNodeData = (nodeId: string, newData: Partial<NodeData>) => {
setNodes((nds) =>
nds.map((node) => {
if (node.id === nodeId) {
return {
...node,
data: { ...node.data, ...newData },
};
}
return node;
})
);
};
レイアウト
React Flowは自動レイアウト機能を持たないため、外部ライブラリと組み合わせて使用します。
dagre(階層レイアウト)
npm install dagre @types/dagre
import dagre from 'dagre';
import { Node, Edge } from 'reactflow';
const dagreGraph = new dagre.graphlib.Graph();
dagreGraph.setDefaultEdgeLabel(() => ({}));
const nodeWidth = 172;
const nodeHeight = 36;
function getLayoutedElements(
nodes: Node[],
edges: Edge[],
direction = 'TB'
) {
const isHorizontal = direction === 'LR';
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);
return {
...node,
position: {
x: nodeWithPosition.x - nodeWidth / 2,
y: nodeWithPosition.y - nodeHeight / 2,
},
};
});
return { nodes: layoutedNodes, edges };
}
elk(高度なレイアウト)
npm install elkjs
import ELK from 'elkjs/lib/elk.bundled.js';
const elk = new ELK();
async function getLayoutedElements(nodes: Node[], edges: Edge[]) {
const elkGraph = {
id: 'root',
layoutOptions: {
'elk.algorithm': 'layered',
'elk.direction': 'DOWN',
},
children: nodes.map((node) => ({
id: node.id,
width: 150,
height: 50,
})),
edges: edges.map((edge) => ({
id: edge.id,
sources: [edge.source],
targets: [edge.target],
})),
};
const layoutedGraph = await elk.layout(elkGraph);
const layoutedNodes = nodes.map((node) => {
const elkNode = layoutedGraph.children?.find((n) => n.id === node.id);
return {
...node,
position: { x: elkNode?.x ?? 0, y: elkNode?.y ?? 0 },
};
});
return { nodes: layoutedNodes, edges };
}
付属コンポーネント
MiniMap
全体のプレビューを表示するミニマップです。
import { MiniMap } from 'reactflow';
<MiniMap
nodeColor={(node) => {
switch (node.type) {
case 'input': return '#6ede87';
case 'output': return '#ff0072';
default: return '#eee';
}
}}
nodeStrokeWidth={3}
zoomable
pannable
/>
Controls
ズームイン/アウト、フィットビューなどのコントロールボタンです。
import { Controls } from 'reactflow';
<Controls
showZoom={true}
showFitView={true}
showInteractive={true}
position="top-left"
/>
Background
背景パターンを表示します。
import { Background, BackgroundVariant } from 'reactflow';
<Background
variant={BackgroundVariant.Dots} // 'dots', 'lines', 'cross'
gap={16}
size={1}
color="#ccc"
/>
Panel
任意のコンテンツを配置するパネルです。
import { Panel } from 'reactflow';
<Panel position="top-right">
<button onClick={handleSave}>保存</button>
</Panel>
イベントハンドリング
ノード関連イベント
<ReactFlow
onNodeClick={(event, node) => console.log('クリック:', node)}
onNodeDoubleClick={(event, node) => console.log('ダブルクリック:', node)}
onNodeDragStart={(event, node) => console.log('ドラッグ開始:', node)}
onNodeDrag={(event, node) => console.log('ドラッグ中:', node)}
onNodeDragStop={(event, node) => console.log('ドラッグ終了:', node)}
onNodeMouseEnter={(event, node) => console.log('マウスエンター:', node)}
onNodeMouseLeave={(event, node) => console.log('マウスリーブ:', node)}
onNodesDelete={(nodes) => console.log('削除:', nodes)}
/>
エッジ関連イベント
<ReactFlow
onEdgeClick={(event, edge) => console.log('エッジクリック:', edge)}
onEdgeDoubleClick={(event, edge) => console.log('エッジダブルクリック:', edge)}
onEdgesDelete={(edges) => console.log('エッジ削除:', edges)}
/>
接続関連イベント
<ReactFlow
onConnect={(connection) => console.log('接続:', connection)}
onConnectStart={(event, { nodeId, handleId }) => console.log('接続開始')}
onConnectEnd={(event) => console.log('接続終了')}
/>
選択関連イベント
<ReactFlow
onSelectionChange={({ nodes, edges }) => console.log('選択変更:', nodes, edges)}
/>
スタイリング
CSSでのスタイリング
/* ノードのスタイル */
.react-flow__node {
border-radius: 8px;
border: 1px solid #ddd;
background: white;
padding: 10px;
}
.react-flow__node-input {
background: #d6f5d6;
border-color: #4caf50;
}
.react-flow__node-output {
background: #f5d6d6;
border-color: #f44336;
}
/* エッジのスタイル */
.react-flow__edge-path {
stroke: #333;
stroke-width: 2;
}
/* 選択時のスタイル */
.react-flow__node.selected {
box-shadow: 0 0 0 2px #1a192b;
}
.react-flow__edge.selected .react-flow__edge-path {
stroke: #1a192b;
}
インラインスタイル
const nodes: Node[] = [
{
id: '1',
data: { label: 'スタイル付きノード' },
position: { x: 100, y: 100 },
style: {
background: '#D6D5E6',
color: '#333',
border: '1px solid #222138',
width: 180,
},
},
];
パフォーマンス最適化
大量ノード対応
<ReactFlow
nodes={nodes}
edges={edges}
nodesDraggable={false} // ドラッグ無効化で軽量化
nodesConnectable={false} // 接続無効化で軽量化
elementsSelectable={false} // 選択無効化で軽量化
minZoom={0.1}
maxZoom={4}
/>
useCallback / useMemo
const nodeTypes = useMemo(() => ({
custom: CustomNode,
}), []);
const onConnect = useCallback(
(connection: Connection) => setEdges((eds) => addEdge(connection, eds)),
[setEdges]
);
マインドマップの実装例
マインドマップを実装する基本的なアプローチです。
import { useCallback, useMemo } from 'react';
import ReactFlow, {
Node,
Edge,
Controls,
Background,
useNodesState,
useEdgesState,
addEdge,
Connection,
} from 'reactflow';
import 'reactflow/dist/style.css';
// マインドマップ用のカスタムノード
function MindMapNode({ data }: { data: { label: string } }) {
return (
<div style={{
padding: '10px 20px',
borderRadius: '20px',
background: data.isRoot ? '#6366f1' : '#e0e7ff',
color: data.isRoot ? 'white' : '#1e1b4b',
fontWeight: data.isRoot ? 'bold' : 'normal',
}}>
{data.label}
</div>
);
}
const nodeTypes = { mindmap: MindMapNode };
// マインドマップのデータ
const initialNodes: Node[] = [
{ id: 'root', type: 'mindmap', data: { label: '中心テーマ', isRoot: true }, position: { x: 300, y: 200 } },
{ id: 'topic1', type: 'mindmap', data: { label: 'トピック1' }, position: { x: 100, y: 100 } },
{ id: 'topic2', type: 'mindmap', data: { label: 'トピック2' }, position: { x: 500, y: 100 } },
{ id: 'topic3', type: 'mindmap', data: { label: 'トピック3' }, position: { x: 100, y: 300 } },
{ id: 'topic4', type: 'mindmap', data: { label: 'トピック4' }, position: { x: 500, y: 300 } },
];
const initialEdges: Edge[] = [
{ id: 'e-root-1', source: 'root', target: 'topic1', type: 'smoothstep' },
{ id: 'e-root-2', source: 'root', target: 'topic2', type: 'smoothstep' },
{ id: 'e-root-3', source: 'root', target: 'topic3', type: 'smoothstep' },
{ id: 'e-root-4', source: 'root', target: 'topic4', type: 'smoothstep' },
];
function MindMap() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback(
(connection: Connection) => setEdges((eds) => addEdge(connection, eds)),
[setEdges]
);
return (
<div style={{ width: '100%', height: '500px' }}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypes}
fitView
>
<Controls />
<Background />
</ReactFlow>
</div>
);
}
export default MindMap;
制限事項
React依存
React Flowを使用するにはReactが必須です。他のフレームワーク(Vue、Svelte)を使用している場合は、Vue FlowやSvelte Flowを検討してください。
自動レイアウトなし
React Flowには組み込みの自動レイアウト機能がありません。dagreやelkjsなどの外部ライブラリと組み合わせる必要があります。
Pro版の機能
一部の高度な機能(Pro版)は有料です。
- ヘルパーライン
- 自動パンニング
- ノードリサイザー
- ノードツールバー