React Flow 触ってみた
はじめに
まだまだフロント開発は初心者ですが、React Flow なるものに出会ったのでどんな感じなのか少し触れてみました。
環境
- Node.js: v22.16.0
- npm: 10.9.2
- OS: macOS
- React: 19.1.0
- React Flow: 11.11.4
React アプリ構築手順
- 基本のReactアプリ作成
npx create-react-app [任意のプロジェクト名]
cd [任意のプロジェクト名]
- React Flow ライブラリのインストール
npm install reactflow
- 開発サーバーの起動
npm start
これで基本的なReact のアプリが作成できました。
React Flow を使ってフローチャートを表示してみよう
-
src/FlowChart.js を作成します。
- 必要なimport
import React, { useCallback } from 'react'; import ReactFlow, { MiniMap, Controls, Background, useNodesState, useEdgesState, addEdge, } from 'reactflow'; import 'reactflow/dist/style.css';
- ノードの定義
const initialNodes = [ { id: '1', position: { x: 0, y: 0 }, data: { label: 'スタート' }, }, { id: '2', position: { x: 0, y: 100 }, data: { label: 'プロセス1' }, }, { id: '3', position: { x: 200, y: 100 }, data: { label: 'プロセス2' }, }, { id: '4', position: { x: 100, y: 200 }, data: { label: '終了' }, }, ];
- エッジの定義
const initialEdges = [ { id: 'e1-2', source: '1', target: '2' }, { id: 'e1-3', source: '1', target: '3' }, { id: 'e2-4', source: '2', target: '4' }, { id: 'e3-4', source: '3', target: '4' }, ];
- コンポーネント
function FlowChart() { const [nodes, setNodes, onNodesChange] = State(initialNodes); const [edges, setEdges, onEdgesChange] = State(initialEdges); const onConnect = useCallback( (params) => setEdges((eds) => addEdge(params, [setEdges], ); return ( <div style={{ width: '100vw', height: '100vh' }}> <ReactFlow nodes={nodes} edges={edges} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onConnect={onConnect} > <Controls /> <MiniMap /> <Background variant="dots" gap={12} size= </ReactFlow> </div> ); } export default FlowChart;
-
FlowChartコンポーネントを表示するようにします。
- src/App.js を以下の内容に書き換え
import React from 'react'; import FlowChart from './FlowChart'; import './App.css'; function App() { return ( <div className="App"> <FlowChart /> </div> ); } export default App;
先ほど React アプリは起動していたので画面を見にいくと、
フローチャート画面が表示されていました!おお〜〜。
ちょっといじってみよう
ノードの追加・削除ができるようにする
この画面に表示されている状態から、ノードの追加・削除ができるようにしたいと思います。src/FlowChart.js の内容を以下に修正します。
import React, { useCallback, useState } from 'react';
import ReactFlow, {
MiniMap,
Controls,
Background,
useNodesState,
useEdgesState,
addEdge,
} from 'reactflow';
import 'reactflow/dist/style.css';
const initialNodes = [
{
id: '1',
position: { x: 0, y: 0 },
data: { label: 'スタート' },
},
{
id: '2',
position: { x: 0, y: 100 },
data: { label: 'プロセス1' },
},
{
id: '3',
position: { x: 200, y: 100 },
data: { label: 'プロセス2' },
},
{
id: '4',
position: { x: 100, y: 200 },
data: { label: '終了' },
},
];
const initialEdges = [
{ id: 'e1-2', source: '1', target: '2' },
{ id: 'e1-3', source: '1', target: '3' },
{ id: 'e2-4', source: '2', target: '4' },
{ id: 'e3-4', source: '3', target: '4' },
];
function FlowChart() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const [nodeCounter, setNodeCounter] = useState(5); // 初期ノード数の次の番号
const [selectedNode, setSelectedNode] = useState(null);
const [editingLabel, setEditingLabel] = useState('');
const onConnect = useCallback(
(params) => setEdges((eds) => addEdge(params, eds)),
[setEdges],
);
// ノード追加機能
const addNode = useCallback(() => {
const newNode = {
id: nodeCounter.toString(),
position: {
x: Math.random() * 400, // ランダムな位置に配置
y: Math.random() * 400
},
data: { label: `新しいノード ${nodeCounter}` },
};
setNodes((nds) => [...nds, newNode]);
setNodeCounter((counter) => counter + 1);
}, [nodeCounter, setNodes]);
// ノード削除機能
const deleteNode = useCallback((nodeId) => {
setNodes((nds) => nds.filter((node) => node.id !== nodeId));
// 削除されたノードに関連するエッジも削除
setEdges((eds) => eds.filter((edge) => edge.source !== nodeId && edge.target !== nodeId));
setSelectedNode(null);
}, [setNodes, setEdges]);
// ノードのクリックで選択
const onNodeClick = useCallback((event, node) => {
setSelectedNode(node);
setEditingLabel(node.data.label);
}, []);
// ノードのダブルクリックで削除
const onNodeDoubleClick = useCallback((event, node) => {
event.preventDefault();
deleteNode(node.id);
}, [deleteNode]);
// ノードラベルの更新
const updateNodeLabel = useCallback(() => {
if (selectedNode && editingLabel.trim()) {
setNodes((nds) =>
nds.map((node) =>
node.id === selectedNode.id
? { ...node, data: { ...node.data, label: editingLabel } }
: node
)
);
setSelectedNode(null);
setEditingLabel('');
}
}, [selectedNode, editingLabel, setNodes]);
// 選択されたノードを削除
const deleteSelectedNode = useCallback(() => {
if (selectedNode) {
deleteNode(selectedNode.id);
}
}, [selectedNode, deleteNode]);
return (
<div style={{ width: '100vw', height: '100vh', position: 'relative' }}>
{/* コントロールパネル */}
<div style={{
position: 'absolute',
top: 10,
left: 10,
zIndex: 1000,
background: 'white',
padding: '15px',
borderRadius: '8px',
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
minWidth: '300px'
}}>
<div style={{ marginBottom: '10px' }}>
<button
onClick={addNode}
style={{
padding: '8px 16px',
marginRight: '10px',
backgroundColor: '#4CAF50',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px'
}}
>
ノード追加
</button>
<span style={{ fontSize: '12px', color: '#666' }}>
ノードをダブルクリックで削除
</span>
</div>
{/* 選択されたノードの編集パネル */}
{selectedNode && (
<div style={{
marginTop: '15px',
padding: '10px',
backgroundColor: '#f5f5f5',
borderRadius: '4px',
border: '1px solid #ddd'
}}>
<h4 style={{ margin: '0 0 10px 0', fontSize: '14px' }}>
選択中: {selectedNode.data.label}
</h4>
<div style={{ marginBottom: '10px' }}>
<input
type="text"
value={editingLabel}
onChange={(e) => setEditingLabel(e.target.value)}
placeholder="ノードのラベル"
style={{
width: '100%',
padding: '6px',
border: '1px solid #ccc',
borderRadius: '4px',
fontSize: '14px'
}}
onKeyPress={(e) => {
if (e.key === 'Enter') {
updateNodeLabel();
}
}}
/>
</div>
<div>
<button
onClick={updateNodeLabel}
style={{
padding: '6px 12px',
marginRight: '8px',
backgroundColor: '#2196F3',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '12px'
}}
>
更新
</button>
<button
onClick={deleteSelectedNode}
style={{
padding: '6px 12px',
backgroundColor: '#f44336',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '12px'
}}
>
削除
</button>
</div>
</div>
)}
</div>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onNodeClick={onNodeClick}
onNodeDoubleClick={onNodeDoubleClick}
fitView
>
<Controls />
<MiniMap />
<Background variant="dots" gap={12} size={1} />
</ReactFlow>
</div>
);
}
export default FlowChart;
ノード追加ボタンが追加されました。ボタンを押すと新しいノードが追加されます。
既存のノードを選択すると、ノード名を編集し更新できるようになっています。
削除ボタンを押すとノードが削除されます。ノードのダブルクリックでも削除できます。
内容解説
インポートとセットアップ
import React, { useCallback, useState } from 'react';
import ReactFlow, {
MiniMap,
Controls,
Background,
useNodesState,
useEdgesState,
addEdge,
} from 'reactflow';
useNodesStateとuseEdgesStateはノードとエッジの状態管理用です。
初期データ定義
const initialNodes = [
{ id: '1', position: { x: 0, y: 0 }, data: { label: 'スタート' } },
// ... 他のノード
];
const initialEdges = [
{ id: 'e1-2', source: '1', target: '2' },
// ... 他のエッジ
];
ノード間を接続するエッジを定義しています。
状態管理
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const [nodeCounter, setNodeCounter] = useState(5);
const [selectedNode, setSelectedNode] = useState(null);
const [editingLabel, setEditingLabel] = useState('');
現在のノードとエッジの状態を管理しています。
nodeCounter で新しいノードを作成する際はユニークIDを生成しています。初期値はすでに4つのノードが存在しているので5を設定しています。ノード追加のたびにインクリメントします。
selectedNode は現在選択されているノードオブジェクトで、nullの場合は何も選択されていない状態です。ノードクリック時に設定され、編集パネルの表示制御に使用します。
editingLabel は選択されたノードのラベルを編集する際の一時的な値で、入力フィールドの値として使用します。更新またはキャンセル時にクリアされます。
主要な機能
ノード追加機能
const addNode = useCallback(() => {
const newNode = {
id: nodeCounter.toString(),
position: {
x: Math.random() * 400,
y: Math.random() * 400
},
data: { label: `新しいノード ${nodeCounter}` },
};
setNodes((nds) => [...nds, newNode]);
setNodeCounter((counter) => counter + 1);
}, [nodeCounter, setNodes]);
ランダムな位置に新しいノードを追加しています。
ノード削除機能
const deleteNode = useCallback((nodeId) => {
setNodes((nds) => nds.filter((node) => node.id !== nodeId));
setEdges((eds) => eds.filter((edge) => edge.source !== nodeId && edge.target !== nodeId));
setSelectedNode(null);
}, [setNodes, setEdges]);
指定されたノードと関連するエッジを削除します。
ノード選択とラベル編集
const onNodeClick = useCallback((event, node) => {
setSelectedNode(node);
setEditingLabel(node.data.label);
}, []);
const updateNodeLabel = useCallback(() => {
if (selectedNode && editingLabel.trim()) {
setNodes((nds) =>
nds.map((node) =>
node.id === selectedNode.id
? { ...node, data: { ...node.data, label: editingLabel } }
: node
)
);
setSelectedNode(null);
setEditingLabel('');
}
}, [selectedNode, editingLabel, setNodes]);
ノードクリックで選択、ラベルの編集が可能です。
UI構成
コントロールパネル
{/* コントロールパネル */}
<div style={{
position: 'absolute',
top: 10,
left: 10,
zIndex: 1000,
background: 'white',
padding: '15px',
borderRadius: '8px',
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
minWidth: '300px'
}}>
<div style={{ marginBottom: '10px' }}>
<button
onClick={addNode}
style={{
padding: '8px 16px',
marginRight: '10px',
backgroundColor: '#4CAF50',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px'
}}
>
ノード追加
</button>
<span style={{ fontSize: '12px', color: '#666' }}>
ノードをダブルクリックで削除
</span>
</div>
{/* 選択されたノードの編集パネル */}
{selectedNode && (
<div style={{
marginTop: '15px',
padding: '10px',
backgroundColor: '#f5f5f5',
borderRadius: '4px',
border: '1px solid #ddd'
}}>
<h4 style={{ margin: '0 0 10px 0', fontSize: '14px' }}>
選択中: {selectedNode.data.label}
</h4>
<div style={{ marginBottom: '10px' }}>
<input
type="text"
value={editingLabel}
onChange={(e) => setEditingLabel(e.target.value)}
placeholder="ノードのラベル"
style={{
width: '100%',
padding: '6px',
border: '1px solid #ccc',
borderRadius: '4px',
fontSize: '14px'
}}
onKeyPress={(e) => {
if (e.key === 'Enter') {
updateNodeLabel();
}
}}
/>
</div>
<div>
<button
onClick={updateNodeLabel}
style={{
padding: '6px 12px',
marginRight: '8px',
backgroundColor: '#2196F3',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '12px'
}}
>
更新
</button>
<button
onClick={deleteSelectedNode}
style={{
padding: '6px 12px',
backgroundColor: '#f44336',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '12px'
}}
>
削除
</button>
</div>
</div>
)}
</div>
- ノード追加ボタン
- 選択されたノードの編集パネル(ラベル変更、削除)
- 画面左上に固定配置
ReactFlowコンポーネント
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onNodeClick={onNodeClick}
onNodeDoubleClick={onNodeDoubleClick}
fitView
>
<Controls />
<MiniMap />
<Background variant="dots" gap={12} size={1} />
</ReactFlow>
- Controls: ズーム、パンなどの操作コントロール
- MiniMap: フローチャート全体の縮小表示
- Background: ドット背景
まとめ
今回作成した機能
- ✅ インタラクティブなフローチャート
- ✅ ノードの追加・削除
- ✅ ノード名の編集
すごく難しいのかと思っていましたが、React Flow を import してコンポーネントを表示させるだけでなんかもうそれっぽいのができたので驚きました。これで組織図とか作ってみたいですね。視覚的に編集できそうだし。
ちなみに、TypeScript を使うと以下が得られるらしく、
- React Flowのプロパティの自動補完
- ノードやエッジの型チェック
- APIの使い方が間違っている場合の警告
今回はお試しでやってみたので Javascript でやっちゃいましたが、より実践的にやる時は TypeScript でやってみようと思います。
Discussion