tech-book-labs
libraries

react-flow-v12-setup

ワークフローエディタ / ER 図 / マインドマップ / DAG ビジュアライザ等の **ノードベース UI** を React で実装する時に呼び出すべき skill。React Flow v12(`@xyflow/react`)で最小コードで立ち上げ、カスタムノード追加、プログラマティックなノード/エッジ追加削除、localStorage 永続化、TypeScript 型(`Node<T>` / `Edge`)、よく詰まる落とし穴(`React.memo` / `useNodesState` 等)までを実装ベースで解説。次のいずれかに該当する時に invoke する: (1) ノード/エッジ/ハンドルが出てくる UI 要件、(2) `@xyflow/react` や `reactflow` の import を含むファイルを編集中、(3) ユーザーが 'flow chart' / 'node editor' / 'graph editor' を作りたいと言及、(4) `<ReactFlow />` の使い方 / 型 / 落とし穴に関する質問。検証バージョン: 12.10.1、検証日: 2026-05-09。

React Flow v12(@xyflow/react)実装 skill

検証バージョン: @xyflow/react@12.10.1 検証日: 2026-05-09 対象: React 19 + TypeScript で @xyflow/react を使う実装作業

ノードベース UI(ワークフロー / マインドマップ / ER 図 / DAG エディタ)を React で構築する時の実装パターン。

まず詰まる 3 点

実装着手前に確認:

  1. コンテナに widthheight を明示しないと描画されない(0px 扱い)。100vh か親要素の固定高さが必要
  2. @xyflow/react/dist/style.css の import 必須。これが無いとハンドル(接続点)が見えない
  3. node の id は string 必須。数値や undefined を渡すと黙ってバグる。crypto.randomUUID() などで明示文字列化

最小サンプル(コピペで動く)

import {
  ReactFlow,
  ReactFlowProvider,
  useNodesState,
  useEdgesState,
  Controls,
  Background,
} from "@xyflow/react";
import "@xyflow/react/dist/style.css";

const initialNodes = [
  { id: "1", position: { x: 0, y: 0 }, data: { label: "入力" } },
  { id: "2", position: { x: 200, y: 100 }, data: { label: "処理" } },
];

function FlowInner() {
  const [nodes, , onNodesChange] = useNodesState(initialNodes);
  const [edges, , onEdgesChange] = useEdgesState([]);

  return (
    <div style={{ width: "100vw", height: "100vh" }}>
      <ReactFlow
        nodes={nodes}
        edges={edges}
        onNodesChange={onNodesChange}
        onEdgesChange={onEdgesChange}
        fitView
      >
        <Controls />
        <Background />
      </ReactFlow>
    </div>
  );
}

export default function Flow() {
  return (
    <ReactFlowProvider>
      <FlowInner />
    </ReactFlowProvider>
  );
}

ReactFlowProvider で囲むと useReactFlow() が使える。つねにラップする クセをつける(後で View 操作したくなる)。

カスタムノード(Handle で接続点を定義)

import { Handle, Position, type NodeProps } from "@xyflow/react";

type MyData = { label: string; status: "ok" | "warn" | "ng" };

export function MyNode({ data }: NodeProps<MyData>) {
  return (
    <div className="rounded border p-2 bg-white">
      <Handle type="target" position={Position.Top} />
      <strong>{data.label}</strong>
      <span data-status={data.status}>{data.status}</span>
      <Handle type="source" position={Position.Bottom} />
    </div>
  );
}

const nodeTypes = { my: MyNode };

// <ReactFlow nodeTypes={nodeTypes} ... />

nodeTypes は memo 化が暗黙の前提。コンポーネント外で定義する(関数内で {} を毎回 new するとレンダループに入る)。

プログラマティックに追加 / 削除

import { useReactFlow } from "@xyflow/react";

function ToolBar() {
  const { addNodes, deleteElements, getNodes } = useReactFlow();

  const add = () =>
    addNodes({
      id: crypto.randomUUID(),
      position: { x: Math.random() * 400, y: Math.random() * 400 },
      data: { label: "new" },
    });

  const removeSelected = () => {
    const selected = getNodes().filter((n) => n.selected);
    deleteElements({ nodes: selected });
  };

  return (
    <>
      <button onClick={add}>追加</button>
      <button onClick={removeSelected}>選択を削除</button>
    </>
  );
}

useReactFlow()ReactFlowProvider 内でしか動かない。プロバイダ外で叩くと無音で失敗する。

ローカル永続化(localStorage)

const { toObject } = useReactFlow();

const onSave = useCallback(() => {
  localStorage.setItem("my-flow", JSON.stringify(toObject()));
}, [toObject]);

const onRestore = useCallback(() => {
  const raw = localStorage.getItem("my-flow");
  if (!raw) return;
  const flow = JSON.parse(raw);
  setNodes(flow.nodes ?? []);
  setEdges(flow.edges ?? []);
  setViewport(flow.viewport);
}, [setNodes, setEdges, setViewport]);

toObject() は viewport も含む。setViewport(flow.viewport) で復元しないとカメラ位置が保存されない。

TypeScript の型

Node / Edge は generic で data の型を絞り込める。

import type { Node, Edge } from "@xyflow/react";

type StepData = { label: string; status: "ok" | "warn" | "ng" };
type StepNode = Node<StepData, "step">;
type FlowEdge = Edge;

const initialNodes: StepNode[] = [
  { id: "1", type: "step", position: { x: 0, y: 0 }, data: { label: "...", status: "ok" } },
];

type: "step"Node<Data, Type> の第 2 引数で固定しておくと、nodeTypes.step の component が必ず NodeProps<StepData> を受ける関係になる。

バージョン間の罠(古い情報を掴みやすい)

  • v11 以前の reactflow パッケージ は別物。検索で出てくる古い記事は v11 系であることが多く、import が違う(from 'reactflow' vs from '@xyflow/react')
  • v11 の reactflow/dist/style.css も別パス
  • useStore の API は v12 で再設計されている。v11 のコードをコピペしてもしばしば動かない

検索する時は @xyflow/react で絞る(reactflow だと v11 系も混ざる)。

パフォーマンス

ノード数体感対策
〜数百余裕デフォルトで OK
〜数千操作で重いonlyRenderVisibleElements を true、ノードを memo 化
1 万〜厳しい別ライブラリ(Cytoscape / Sigma)検討 or 仮想化

data を mutate すると React Flow が変更を検知できない(参照が同じだから)。必ず新しいオブジェクトを作る({ ...prev, label: "new" })。

向く / 向かないケース

  • 向く: ワークフローエディタ、ER 図、データフロー UI、設定 UI、マインドマップ
  • 向かない: 1 万ノード級の可視化、純粋な静的図(Mermaid で十分)、複雑なグラフ計算(d3-force と組み合わせる必要)

See also(任意、単独でも完結)

本 skill は React Flow の実装に特化しており、単独で完結します。記事化する場合は本リポジトリの公開版編集方針 src/content/methodology/01-editorial-principles を参照。

参考