TypeScriptとReact Flowの併用

React Flowは、TypeScriptで記述されています。これは、TypeScriptが提供する追加の安全障壁を重視しているためです。React Flowコンポーネントに渡すデータ構造と関数を正しく型指定するために必要なすべての型をエクスポートします。また、ノードとエッジの型を拡張する方法も提供します。

基本的な使用方法

まずは、シンプルな開始点に必要な最も基本的な型から始めましょう。TypeScriptはこれらの型のいくつかをすでに推測しているかもしれませんが、ここでは明示的に定義します。

import { useState, useCallback } from 'react';
import {
  ReactFlow,
  addEdge,
  applyNodeChanges,
  applyEdgeChanges,
  type Node,
  type Edge,
  type FitViewOptions,
  type OnConnect,
  type OnNodesChange,
  type OnEdgesChange,
  type OnNodeDrag,
  type NodeTypes,
  type DefaultEdgeOptions,
} from '@xyflow/react';
 
const initialNodes: Node[] = [
  { id: '1', data: { label: 'Node 1' }, position: { x: 5, y: 5 } },
  { id: '2', data: { label: 'Node 2' }, position: { x: 5, y: 100 } },
];
 
const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', target: '2' }];
 
const fitViewOptions: FitViewOptions = {
  padding: 0.2,
};
 
const defaultEdgeOptions: DefaultEdgeOptions = {
  animated: true,
};
 
const nodeTypes: NodeTypes = {
  num: NumberNode,
  txt: TextNode,
};
 
const onNodeDrag: OnNodeDrag = (_, node) => {
  console.log('drag event', node.data);
};
 
function Flow() {
  const [nodes, setNodes] = useState<Node[]>(initialNodes);
  const [edges, setEdges] = useState<Edge[]>(initialEdges);
 
  const onNodesChange: OnNodesChange = useCallback(
    (changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
    [setNodes],
  );
  const onEdgesChange: OnEdgesChange = useCallback(
    (changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
    [setEdges],
  );
  const onConnect: OnConnect = useCallback(
    (connection) => setEdges((eds) => addEdge(connection, eds)),
    [setEdges],
  );
 
  return (
    <ReactFlow
      nodes={nodes}
      nodeTypes={nodeTypes}
      edges={edges}
      edgeTypes={edgeTypes}
      onNodesChange={onNodesChange}
      onEdgesChange={onEdgesChange}
      onConnect={onConnect}
      onNodeDrag={onNodeDrag}
      fitView
      fitViewOptions={fitViewOptions}
      defaultEdgeOptions={defaultEdgeOptions}
    />
  );
}

カスタムノード

カスタムノード を使用する場合は、カスタム Node 型(または Node 共用体)を NodeProps 型に渡すことができます。カスタムノードを使用するには、基本的に2つの方法があります。

  1. **複数**のカスタムノードがある場合は、特定の Node 型をジェネリックとして NodeProps 型に渡します。
NumberNode.tsx
import type { Node, NodeProps } from '@xyflow/react';
 
type NumberNode = Node<{ number: number }, 'number'>;
 
export default function NumberNode({ data }: NodeProps<NumberNode>) {
  return <div>A special number: {data.number}</div>;
}

⚠️ ノードデータを個別に指定する場合は、type を使用する必要があります(interface はここでは機能しません)。

type NumberNodeData = { number: number };
type NumberNode = Node<NumberNodeData, 'number'>;
  1. ノードタイプに基づいて異なるコンテンツをレンダリングする**1つ**のカスタムノードがある場合は、Node 共用体型をジェネリックとして NodeProps に渡します。
CustomNode.tsx
import type { Node, NodeProps } from '@xyflow/react';
 
type NumberNode = Node<{ number: number }, 'number'>;
type TextNode = Node<{ text: string }, 'text'>;
 
type AppNode = NumberNode | TextNode;
 
export default function CustomNode({ data }: NodeProps<AppNode>) {
  if (data.type === 'number') {
    return <div>A special number: {data.number}</div>;
  }
 
  return <div>A special text: {data.text}</div>;
}

カスタムエッジ

カスタムエッジ については、カスタムノードと同じ可能性があります。

CustomEdge.tsx
import {
  getStraightPath,
  BaseEdge,
  type EdgeProps,
  type Edge,
} from '@xyflow/react';
 
type CustomEdge = Edge<{ value: number }, 'custom'>;
 
export default function CustomEdge({
  id,
  sourceX,
  sourceY,
  targetX,
  targetY,
}: EdgeProps<CustomEdge>) {
  const [edgePath] = getStraightPath({ sourceX, sourceY, targetX, targetY });
 
  return <BaseEdge id={id} path={edgePath} />;
}

高度な使用方法

React Flowを使用して複雑なアプリケーションを作成する場合、それぞれに異なる種類のデータがアタッチされた多数のカスタムノードとエッジが必要になります。組み込み関数とフックを介してこれらのノードとエッジを操作する場合、実行時エラーを防ぐために、ノードとエッジの型を絞り込む必要があります。

Node 型と Edge 型の共用体

NodeType または EdgeType ジェネリックを期待する多くの関数、コールバック、フック(ReactFlowコンポーネント自体も)が表示されます。これらのジェネリックは、アプリケーションにあるすべての異なるタイプのノードとエッジの共用体です。データオブジェクトを正しく型指定している限り(前のセクションを参照)、エクスポートされた型を使用できます。

組み込みノード(「input」、「output」、「default」)またはエッジ(「straight」、「step」、「smoothstep」、「bezier」)のいずれかを使用する場合は、@xyflow/react からエクスポートされた BuiltInNode 型と BuiltInEdge 型を共用体型に追加できます。

import type { BuiltInNode, BuiltInEdge } from '@xyflow/react';
 
// Custom nodes
import NumberNode from './NumberNode';
import TextNode from './TextNode';
 
// Custom edge
import EditableEdge from './EditableEdge';
 
export type CustomNodeType = BuiltInNode | NumberNode | TextNode;
export type CustomEdgeType = BuiltInEdge | EditableEdge;

<ReactFlow /> に渡される関数

コールバック関数の正しい型を受け取るには、共用体型を ReactFlow コンポーネントに渡すことができます。そうすることで、コールバック関数を明示的に型指定する必要があります。

import { type OnNodeDrag } from '@xyflow/react';
 
// ...
 
// Pass your union type here ...
const onNodeDrag: OnNodeDrag<CustomNodeType> = useCallback((_, node) => {
  if (node.type === 'number') {
    // From here on, Typescript knows that node.data
    // is of type { num: number }
    console.log('drag event', node.data.number);
  }
}, []);
 
const onNodesChange: OnNodesChange<CustomNodeType> = useCallback(
  (changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
  [setNodes],
);

フック

型共用体は、多くのフックの戻り値の型指定にも使用できます。

FlowComponent.tsx
import {
  useReactFlow,
  useHandleConnections,
  useNodesData,
  useStore,
} from '@xyflow/react';
 
export default function FlowComponent() {
  // returned nodes and edges are correctly typed now
  const { getNodes, getEdges } = useReactFlow<CustomNodeType, CustomEdgeType>();
 
  // You can type useStore by typing the selector function
  const nodes = useStore((s: ReactFlowState<CustomNodeType>) => ({
    nodes: s.nodes,
  }));
 
  const connections = useHandleConnections({
    type: 'target',
  });
 
  const nodesData = useNodesData<CustomNodeType>(connections?.[0].source);
 
  nodeData.forEach(({ type, data }) => {
    if (type === 'number') {
      // This is type safe because we have narrowed down the type
      console.log(data.number);
    }
  });
  // ...
}

型ガード

TypeScriptでは、型ガード を定義する方法は複数あります。isNumberNodeisTextNode などの型ガード関数を定義して、ノードのリストから特定のノードをフィルタリングする方法があります。

function isNumberNode(node: CustomNodeType): node is NumberNode {
  return node.type === 'number';
}
 
// numberNodes is of type NumberNode[]
const numberNodes = nodes.filter(isNumberNode);