Skip to content

Latest commit

History

History
258 lines (198 loc) 路 7.68 KB

typescript.mdx

File metadata and controls

258 lines (198 loc) 路 7.68 KB
title description created_at
Usage with TypeScript
In this guide we exlpain how to work with React Flow and TypeScript.
2024-03-22

import { Callout } from 'nextra/components';

Usage with TypeScript

React Flow is written in TypeScript because we value the additional safety barrier it provides. We export all the types you need for correctly typing data structures and functions you pass to the React Flow component. We also provide a way to extend the types of nodes and edges.

Basic usage

Let's start with the most basic types you need for a simple starting point. Typescript might already infer some of these types, but we will define them explicitly nontheless.

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}
    />
  );
}

Custom nodes

When working with custom nodes you have the possibility to pass a custom Node type (or your Node union) to the NodeProps type. There are basically two ways to work with custom nodes:

  1. If you have multiple custom nodes, you want to pass a specific Node type as a generic to the NodeProps type:
import type { 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>;
}
  1. If you have one custom node that renders different content based on the node type, you want to pass your Node union type as a generic to NodeProps:
import type { 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>;
}

Custom edges

For custom edges you have the same possiblity as for custom nodes.

import { getStraightPath, BaseEdge, type EdgeProps } 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} />;
}

Advanced usage

When creating complex applications with React Flow, you will have a number of custom nodes & edges, each with different kinds of data attached to them. When we operate on these nodes & edges through built in functions and hooks, we have to make sure that we narrow down the types of nodes & edges to prevent runtime errors.

Node and Edge type unions

You will see many functions, callbacks and hooks (even the ReactFlow component itself) that expect a NodeType or EdgeType generic. These generics are simply unions of all the different types of nodes & edges you have in your application. As long as you have typed the data objects correctly (see previous section), you can use their exported type.

If you use any of the built-in nodes ('input', 'output', 'default') or edges ('straight', 'step', 'smoothstep', 'bezier'), you can add the `BuiltInNode` and `BuiltInEdge` types exported from `@xyflow/react` to your union type.
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;

Functions passed to <ReactFlow />

To receive correct types for callback functions, you can pass your union types to the ReactFlow component. By doing that you will have to type your callback functions explicitly.

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],
);

Hooks

The type unions can also be used to type the return values of many hooks.

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);
    }
  });
  // ...
}

Type guards

There are multiple ways you can define type guards in Typescript. One way is to define type guard functions like isNumberNode or isTextNode to filter out specific nodes from a list of nodes.

function isNumberNode(node: CustomNodeType): node is NumberNode {
  return node.type === 'number';
}

// numberNodes is of type NumberNode[]
const numberNodes = nodes.filter(isNumberNode);