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版)は有料です。

  • ヘルパーライン
  • 自動パンニング
  • ノードリサイザー
  • ノードツールバー

参考資料