フローチャートを React らしく手軽に実装できる React Flow の紹介
こんにちは、steshima です。
業務で React Flow に触る機会があったので、今回 React Flow の基本的な使い方を記事にしました。
React Flow のバージョンは 12.6.4
です。
React Flow について
React Flow は、React アプリ上でノードベースの UI を構築できるライブラリです。ドラッグ&ドロップ・ズーム・ノード間接続など、インタラクティブなフローチャートが比較的簡単に作れます。
基本的な使い方
React Flow の基本的な使い方を紹介します。
Controlled or Uncontrolled
React Flow は状態管理の方法に Controlled と Uncontrolled の2種類があります。
簡単に動かすだけなら Uncontrolled で十分ですが、よりインタラクティブな実装をする場合は Controlled で扱う必要があり、公式でも Controlled での実装をおすすめしています。
この記事でも Controlled で扱う方法で紹介します。
初期設定
まずはパッケージをインストールします。
npm install @xyflow/react
あとは CSS の読み込みと、ReactFlow
コンポーネントを実装すればフローチャートを表示できます。
import '@xyflow/react/dist/style.css';
import {
Edge,
Node,
ReactFlow,
useEdgesState,
useNodesState,
} from '@xyflow/react';
const Flow: React.FC = () => {
const initialNodes: Node[] = [
{
id: 'node1',
data: {},
position: { x: 0, y: 0 },
},
{
id: 'node2',
data: {},
position: { x: 0, y: 100 },
},
];
const initialEdges: Edge[] = [
{
id: 'edge1',
source: 'node1',
target: 'node2',
},
];
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
return (
<div style={{ width: '100vw', height: '100vh' }}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
/>
</div>
);
};
2つのノードを一本のエッジで繋いだチャートが描画できました。
ノードとエッジの紐付けは、エッジオブジェクトの source
, target
プロパティでノードの ID を指定するだけです。
ノードオブジェクトの data
プロパティのオブジェクトには任意の値を設定することができますが、使い方は後ほどノードとエッジのカスタマイズで紹介します。
useNodesState
, useEdgesState
は React の useState
に onNodesChange
, onEdgesChange
といったヘルパー関数が付随したものと考えれば大丈夫です。
ノードやエッジを動的にセットする場合、setNodes
, setEdges
を使います。
下記はボタンを押した時にノードとエッジをセットして表示する例です。
const Flow: React.FC = () => {
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
return (
<div style={{ width: '100vw', height: '100vh' }}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}>
<Panel position="top-left">
<button
onClick={() => {
setNodes([
{
id: 'node1',
data: {},
position: { x: 0, y: 0 },
},
{
id: 'node2',
data: {},
position: { x: 0, y: 100 },
},
]);
setEdges([
{
id: 'edge1',
source: 'node1',
target: 'node2',
},
]);
}}>
ノードを表示
</button>
</Panel>
</ReactFlow>
</div>
);
};
ReactFlow
Component Props
ReactFlow
コンポーネントには多くの props が定義されており、細かに設定できます。
よく使いそうなものを一部抜粋。
<ReactFlow
// 画面に収まるように中央寄せでズームレベルを調整して表示する
fitView
// 表示領域の座標・ズーム設定
// `fitView` が設定されている場合、無視される
defaultViewport={{
// 下記デフォルト値
x: 0,
y: 0,
zoom: 1,
}}
// ノードのドラッグ操作禁止
nodesDraggable={false}
// 指定したキーを押しながらクリックすると複数のノードとエッジを選択状態にできる
// デフォルトは macOS であれば cmd。null で複数選択不可にできる
multiSelectionKeyCode={null}
onNodeClick={(_, node) => {}}
onEdgeClick={(_, edge) => {}}
// ノード・エッジ以外の React Flow 描画領域をクリックした時
onPaneClick={() => {}}
/>
ノードとエッジのカスタマイズ
エッジはともかく、ノードを自前でカスタマイズしたいことがよくあると思います。
React らしくカスタマイズしたノードコンポーネントを React Flow で使うことができます。
import {
Edge,
Handle,
Node,
NodeProps,
Position,
ReactFlow,
useEdgesState,
useNodesState,
useReactFlow,
} from '@xyflow/react';
type CustomNode = Node<{ readonly label: string }, 'customNode'>
const CustomNode: React.FC<
NodeProps<CustomNode>
> = ({ data }) => (
<>
<div
style={{
padding: 12,
borderRadius: 8,
border: '2px solid #4f46e5',
background: '#eef2ff',
fontWeight: 'bold',
}}>
{data.label}
</div>
{/* エッジの接続点の設定 */}
<Handle type="source" position={Position.Bottom} />
<Handle type="target" position={Position.Top} />
</>
);
const Flow: React.FC = () => {
const initialNodes: CustomNode[] = [
{
id: 'node1',
data: { label: 'ノード1' },
position: { x: 0, y: 0 },
type: 'customNode',
},
{
id: 'node2',
data: { label: 'ノード2' },
position: { x: 0, y: 100 },
type: 'customNode',
},
];
const initialEdges: Edge[] = [
{
id: 'edge1',
source: 'node1',
target: 'node2',
},
];
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
return (
<div style={{ width: '100vw', height: '100vh' }}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
nodeTypes={{
customNode: CustomNode,
}}
/>
</div>
);
};
ノードのオブジェクトに type
を設定し、どの type とコンポーネントを紐づけるかを ReactFlow
コンポーネントの nodeTypes
prop で設定します。
カスタムコンポーネントにデータを渡したい場合、ノードオブジェクトの data
プロパティにオブジェクトで任意のデータを渡すことで、カスタムコンポーネント側で受け取ることができます。
エッジの場合も同様です。
自動レイアウト
ノードの配置はノードオブジェクトに設定した position
の位置で決まります。
const initialNodes: Node[] = [
{
id: 'node1',
data: {},
position: { x: 0, y: 0 },
},
{
id: 'node2',
data: {},
position: { x: 0, y: 100 },
},
];
動的にレイアウトを設定したいケースがよくあると思いますが、自前でロジックを組むのは手間がかかります。
React Flow 公式でレイアウト用ライブラリと組み合わせた自動レイアウトの例を紹介しており、その中の1つの dagre.js を使った実装を紹介します。
// 下記は React Flow の公式からほとんどそのまま持ってきたもの
// https://reactflow.dev/learn/layouting/layouting#dagre
const getLayoutedElements = (nodes: Node[], edges: Edge[]) => {
const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
g.setGraph({
rankdir: 'TB', // 上から下に並べる
ranksep: 50, // ノード(ランク)同士の間隔
});
edges.forEach((edge) => g.setEdge(edge.source, edge.target));
nodes.forEach((node) =>
g.setNode(node.id, {
...node,
width: node.measured?.width ?? 0,
height: node.measured?.height ?? 0,
}),
);
Dagre.layout(g);
return {
nodes: nodes.map((node) => {
const position = g.node(node.id);
// We are shifting the dagre node position (anchor=center center) to the top left
// so it matches the React Flow node anchor point (top left).
const x = position.x - (node.measured?.width ?? 0) / 2;
const y = position.y - (node.measured?.height ?? 0) / 2;
return { ...node, position: { x, y } };
}),
edges,
};
};
const Flow: React.FC = () => {
const initialNodes: Node[] = [
{
id: 'node1',
data: {},
position: { x: 0, y: 0 },
},
{
id: 'node2',
data: {},
position: { x: 0, y: 0 },
},
];
const initialEdges: Edge[] = [
{
id: 'edge1',
source: 'node1',
target: 'node2',
},
];
const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(
initialNodes,
initialEdges,
);
const [nodes, setNodes, onNodesChange] = useNodesState(layoutedNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(layoutedEdges);
return (
<div style={{ width: '100vw', height: '100vh' }}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
/>
</div>
);
};
ノードオブジェクトには position: { x: 0, y: 0 }
を設定していますが、上記画像のように dagre.js が良い感じに自動で配置してくれています。
処理の流れとしては、dagre.js でノードの座標を調整した新たなノードを state に set するイメージです。
注意点として node.measured?.width
でノードのサイズを動的に取得していますが、一度ノードが render されていなければサイズが取得できません。
なのでサイズが決まっていないノード(ラベルに応じてサイズが変わるなど)の場合、一度 render してから dagre.js によるレイアウトをする必要があるため、一工夫が必要です。
イマイチですが、座標やスタイルを調整して一度ユーザーの目に見えない形で render し、その後レイアウトするような実装しかないかもしれません🤔
フック
React Flow には便利なフックがいくつも用意されており、より複雑な機能を実装する場合に役立ちます。
ここではいくつかフックを紹介します。
まず、フックが React Flow の内部状態にアクセスできるように ReactFlow
を ReactFlowProvider
でラップします。
import {
ReactFlow,
ReactFlowProvider,
} from '@xyflow/react';
<ReactFlowProvider>
<ReactFlow />
</ReactFlowProvider>
ノード・エッジの状態取得・操作
useReactFlow
を使うことでノード・エッジの状態を取得したり、操作することができます。また、他にも Viewport の操作などもできます。
React Flow を操作する基本的な API を提供しているフックで、フックの中でも使う頻度が高そうです。
import {
Edge,
Node,
Panel,
ReactFlow,
useEdgesState,
useNodesState,
useReactFlow,
} from '@xyflow/react';
const Flow: React.FC = () => {
const initialNodes: Node[] = [
{
id: 'node1',
data: {},
position: { x: 0, y: 0 },
width: 150,
},
{
id: 'node2',
data: {},
position: { x: 0, y: 100 },
width: 150,
},
];
const initialEdges: Edge[] = [
{
id: 'edge1',
source: 'node1',
target: 'node2',
},
];
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const { setCenter } = useReactFlow();
return (
<div style={{ width: '100vw', height: '100vh' }}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}>
<Panel position="top-left">
<button onClick={() => setCenter(75, 50, { zoom: 1 })}>
画面中央に表示
</button>
</Panel>
</ReactFlow>
</div>
);
};
現在選択しているノードを取得する
useOnSelectionChange
を使うとノードの選択状態の変更を検知できるため、現在選択中のノードの取得などが可能です。
export const useSelectedNode = (): Node | null => {
const [node, setNode] = useState<Node | null>(null);
// 公式ドキュメントの通り、フックを動作させるためにメモ化する
// https://reactflow.dev/api-reference/hooks/use-on-selection-change
const onChange = useCallback(
(params: OnSelectionChangeParams<Node, Edge>) => {
// 複数選択不可の場合の実装
setNode(params.nodes[0] ?? null);
},
[],
);
useOnSelectionChange({ onChange });
return node;
};
無償版と有償版の違い
React Flow には料金体系として、有償版である React Flow Pro が用意されています。
ありがたいことにライブラリの機能については無償版でも全て使うことができ、React Flow Pro は issue の優先的な解決などサポートをしてくれるようになります。
また、より複雑な実装を行うためのサンプルやテンプレートへのアクセスも可能になります。
React Flow は下記画像のように画面にクレジットが表示されますが、収益を得たり組織で使用していて表記を削除したい場合は React Flow Pro をサブスクリプションしてねと呼びかけています。(React Flow Remove Attribution)
さいごに
今回の記事では基礎的な使い方を中心に紹介しました。
コードを見てもらえればわかる通り React Flow は React らしいコードの書きっぷりで実装できて、機能も豊富な非常にリッチなライブラリになっています。
他のライブラリは詳しく比較していませんが、フローチャートを React で実装するなら React Flow が定番なのかなという印象で、非常に使いやすかったです。
この記事が、React Flow に初めて触れる方の参考になれば幸いです。
Discussion