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';
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.
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}
/>
);
}
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:
- If you have multiple custom nodes, you want to pass a specific
Node
type as a generic to theNodeProps
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>;
}
- 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 toNodeProps
:
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>;
}
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} />;
}
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.
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.
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;
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],
);
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);
}
});
// ...
}
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);