Developing node-based UI with Reactflow

4th January 2023

0

Page Index

    Share

    Introduction

    Reactflow is a JavaScript library that allows developers to create interactive, node-based user interfaces. It is built on top of the popular React library and is designed to be easy to use and customize. With Reactflow, developers can create nodes that represent different pieces of data or functionality, and connect them together with edges to create a flow of data or actions. This makes it easy to build interactive applications that allow users to manipulate data or perform tasks in a visual way.

    Applications

    Workflow builder - Onesignal

    Interactive Process documentation - Stripe

    Visualizing complex logic - Typeform

    Orchestrating functions and integrations - Baseten

    Dependency visualizer - Bit

    Building a Flow

    Creating Nodes

    To create a node in Reactflow, you will need to import the Node component from the Reactflow library and use it in your React component. The Node component takes several props, including a label prop that is used to display text on the node and a data prop that can be used to store any data that you want to associate with the node.

    For example, you could create a node like this:

    import React from "react";
    import ReactFlow, { Controls, Background } from "reactflow";
    import "reactflow/dist/style.css";
    
    const nodes = [
      {
        id: "1",
        position: { x: 0, y: 0 },
        data: { label: "Node" },
      },
    ];
    
    function Node() {
      return (
        <div style={{ height: "100%" }}>
          <ReactFlow nodes={nodes}>
            <Background />
            <Controls />
          </ReactFlow>
        </div>
      );
    }
    
    export default Node;
    
    
    Create Node

    This would create a node with the label "Flow". You can customize the appearance of the node by using the various style props that are available, such as color, borderColor, and borderWidth. You would have noticed that the node is placed at (0,0).

    More nodes can be added by either increasing the initial nodes provided or pushing node objects into the node array.

    Assigning Edges

    To connect nodes together and create a flow of data or actions, you can use the Edge component from the Reactflow library. The Edge component takes two props: source and target, which are used to specify the nodes that the edge should connect.

    For example, you could create an edge like this:

    import ReactFlow, { Controls, Background } from 'reactflow';
    import 'reactflow/dist/style.css';
    
    const edges = [{ id: '1-2', source: '1', target: '2' }];
    
    const nodes = [
      {
        id: '1',
        data: { label: 'Hello' },
        position: { x: 0, y: 0 },
        type: 'input',
      },
      {
        id: '2',
        data: { label: 'World' },
        position: { x: 100, y: 100 },
      },
    ];
    
    function Flow() {
      return (
        <div style={{ height: '100%' }}>
          <ReactFlow nodes={nodes} edges={edges}>
            <Background />
            <Controls />
          </ReactFlow>
        </div>
      );
    }
    
    export default Flow;
    
    
    Create Edge

    This would create an edge that connects the node with the ID "1" to the node with the ID "2". You can customize the appearance of the edge by using the various style props that are available, such as color and width.

    Now, the flow has been created successfully.

    Adding Interactivity

    Reactflow provides several props that you can use to add interactivity to your node-based user interfaces. For example, you can use the onNodesChange prop to specify a function that should be called when any action is taken on a node.. This function can be used to display additional information about the node or to perform some other action.

    You can also use the onEdgesChange prop to specify a function that should be called when any action is taken on an edge. This function can be used to display additional information about the edge or to perform some other action.

    const onNodesChange = useCallback(
      (changes: NodeChange[]) =>
        setNodes((nds) => applyNodeChanges(changes, nds)),
      [setNodes]
    );
    const onEdgesChange = useCallback(
      (changes: EdgeChange[]) =>
        setEdges((eds) => applyEdgeChanges(changes, eds)),
      [setEdges]
    );
      
    <ReactFlow
      nodes={nodes}
      edges={edges}
      onNodesChange={onNodesChange} // nodeChanges here
      onEdgesChange={onEdgesChange} // edgeChanges here
    >
            
    

    Creating Custom Nodes

    One of the key features of Reactflow is the ability to create custom nodes that represent different pieces of data or functionality. To create a custom node, you will need to use the Node component and provide your own content inside the node.

    For example, you could create a custom node like this:

    import { useCallback } from "react";
    
    const handleStyle = { left: 10 };
    
    function CustomNodeTest({ data }: any) {
      const onChange = useCallback((evt: any) => {
        console.log(evt.target.value);
      }, []);
    
      return (
        <div className="p-2 rounded border border-slate-800">
          <label htmlFor="text">Custom Node with text input: </label>
          <input id="text" name="text" onChange={onChange} />
        </div>
      );
    }
    
    export default CustomNodeTest;
    
    

    Then import this node in our main file and include this in the nodes array with its type attribute as the function/component name.

    import CustomNodeTest from "./myCustomNode";
    
    const nodeTypes = useMemo(() => ({ customNode: CustomNodeTest }), []);
    
    const nodes = [
      {
        id: "node-1",
        type: "MyCustomNode",
        position: { x: 0, y: 0 },
        data: { value: 123 },
      },
    ];
    
    
    <ReactFlow
      nodes={nodes}
      edges={edges}
      onNodesChange={onNodesChange}
      onEdgesChange={onEdgesChange}
      nodeTypes={nodeTypes} // nodeTypes here
    >
    
    
    
    Create Edge

    NodeTypes are memoized or it should be outside the component definition. Otherwise there would be performance issues.

    You can customize the appearance of the custom node by using the various style props that are available, such as color, borderColor, and borderWidth.

    Adding a Sub Flow

    In addition to creating a flow with multiple nodes and edges, you can also create a sub flow by using the Flow component from the Reactflow library. A sub flow is a flow within a flow, and can be used to represent a subprocess or a group of related nodes. To create a sub flow, you will need to wrap your nodes and edges in a Flow component and provide a label prop to display a label on the sub flow.

    For example, you could create a sub flow like this:

        {
          id: "parent_group1",
          type: "group",
          data: {
            label: "parent 1",
          },
          position: {
            x: 250,
            y: 250,
          },
          style: {
            width: 200,
            height: 200,
          },
        },
        {
          id: "child1_group1",
          type: "input",
          data: { label: "child node 1" },
          position: { x: 10, y: 10 },
          parentNode: "parent_group1",
          extent: "parent",
        },
        {
          id: "child2_group1",
          data: { label: "child node 2" },
          position: { x: 10, y: 90 },
          parentNode: "parent_group1",
          extent: "parent",
        }
    
    Create Edge

    Notice the 'type' of first node and 'parentNode' of the other two.

    Example Code

    index.tsx

    import Head from "next/head";
    import Image from "next/image";
    import { Inter } from "@next/font/google";
    import styles from "../styles/Home.module.css";
    import { useCallback, useMemo, useState } from "react";
    import ReactFlow, {
      MiniMap,
      Controls,
      Background,
      useNodesState,
      useEdgesState,
      addEdge,
      applyEdgeChanges,
      applyNodeChanges,
      NodeChange,
      EdgeChange,
      Connection,
      Edge,
      MarkerType,
    } from "reactflow";
    // 👇 you need to import the reactflow styles
    import "reactflow/dist/style.css";
    import { Button } from "primereact/button";
    import { Dialog } from "primereact/dialog";
    import { InputText } from "primereact/inputtext";
    import { InputNumber } from "primereact/inputnumber";
    
    import CustomNodeTest from "./myCustomNode";
    
    const inter = Inter({ subsets: ["latin"] });
    
    export default function Home() {
      const initialNodes = [
        {
          id: "1",
          type: "input",
          position: { x: 100, y: 100 },
          data: { label: "Styled First Node" },
          style: {
            color: "white",
            background: "purple",
            padding: "2px",
            borderRadius: "4px",
            width: "75px",
            fontSize: "10px",
          },
        },
        {
          id: "2",
          position: { x: 100, y: 500 },
          data: { label: "Second" },
          type: "output",
        },
        {
          id: "3",
          position: { x: 350, y: 500 },
          data: { label: "Third" },
          type: "output",
        },
        {
          id: "parent_group1",
          type: "group",
          data: {
            label: "parent 1",
          },
          position: {
            x: 250,
            y: 250,
          },
          style: {
            width: 200,
            height: 200,
          },
        },
        {
          id: "child1_group1",
          type: "input",
          data: { label: "child node 1" },
          position: { x: 10, y: 10 },
          parentNode: "parent_group1",
          extent: "parent",
        },
        {
          id: "child2_group1",
          data: { label: "child node 2" },
          position: { x: 10, y: 90 },
          parentNode: "parent_group1",
          extent: "parent",
        },
        {
          id: "custom-node-1",
          type: "customNode",
          position: { x: 200, y: 200 },
          data: { value: 123 },
        },
      ];
      const nodeTypes = useMemo(() => ({ customNode: CustomNodeTest }), []);
    
      const initialEdges = [
        {
          id: "e1-2",
          source: "1",
          target: "2",
          label: "Edge with fill, color, arrowType",
          labelStyle: { fill: "red", fontWeight: 700 },
          labelBgPadding: [8, 4],
          labelBgBorderRadius: 4,
          labelBgStyle: { fill: "#FFCC00", color: "#fff", fillOpacity: 0.7 },
          markerEnd: {
            type: MarkerType.ArrowClosed,
          },
        },
      ];
      const [displayDialog, setDisplayDialog] = useState<boolean>(false);
      const [nodeName, setNodeName] = useState<string>("");
      const [xPosition, setXPosition] = useState<number | null>(null);
      const [yPosition, setYPosition] = useState<number | null>(null);
    
      const [nodes, setNodes] = useState(initialNodes);
      const [edges, setEdges] = useState(initialEdges);
    
      const onNodesChange = useCallback(
        (changes: NodeChange[]) =>
          setNodes((nds) => applyNodeChanges(changes, nds)),
        [setNodes]
      );
      const onEdgesChange = useCallback(
        (changes: EdgeChange[]) =>
          setEdges((eds) => applyEdgeChanges(changes, eds)),
        [setEdges]
      );
      const onConnect = useCallback(
        (connection: Edge<any> | Connection) =>
          setEdges((eds) => addEdge(connection, eds)),
        [setEdges]
      );
    
      const renderFooter = () => {
        return (
          <div>
            <Button
              label="No"
              icon="pi pi-times"
              onClick={() => setDisplayDialog(false)}
              className="p-button-text"
            />
            <Button
              label="Yes"
              icon="pi pi-check"
              onClick={() => {
                handleSetupNode();
              }}
              autoFocus
            />
          </div>
        );
      };
    
      const handleAddNode = () => {
        setDisplayDialog(true);
      };
    
      const handleSetupNode = () => {
        if (xPosition && yPosition && nodeName) {
          setNodes([
            ...nodes,
            {
              id: `${nodes.length + 1}`,
              position: { x: xPosition ?? 0, y: yPosition ?? 0 },
              data: { label: nodeName },
              type: "default",
            },
          ]);
          setDisplayDialog(false);
        } else return;
      };
    
      return (
        <>
          <Head>
            <title>Create Next App</title>
            <meta name="description" content="Generated by create next app" />
            <meta name="viewport" content="width=device-width, initial-scale=1" />
            <link rel="icon" href="/favicon.ico" />
          </Head>
          <div className="absolute top-0 right-0 z-[9999]">
            <Button
              label="Add Node"
              className="cursor-pointer"
              icon="pi pi-plus"
              onClick={handleAddNode}
            />
          </div>
          <Dialog
            header="Add Node"
            visible={displayDialog}
            style={{ width: "50vw" }}
            footer={renderFooter()}
            onHide={() => setDisplayDialog(false)}
          >
            <div className="flex flex-col gap-4">
              <div className="pt-6">
                <span className="p-float-label">
                  <InputText
                    id="nameOfNode"
                    value={nodeName}
                    onChange={(e) => setNodeName(e.target.value)}
                  />
                  <label htmlFor="nameOfNode">Node Name: </label>
                </span>
              </div>
              <div className="flex gap-4 pt-6">
                <span className="p-float-label">
                  <InputNumber
                    id="xPos"
                    value={xPosition}
                    onChange={(e) => {
                      setXPosition(e.value!);
                    }}
                  />
                  <label htmlFor="xPos">X Position: </label>
                </span>
                <span className="p-float-label">
                  <InputNumber
                    id="yPos"
                    value={yPosition}
                    onChange={(e) => setYPosition(e.value!)}
                  />
                  <label htmlFor="yPos">Y Position: </label>
                </span>
              </div>
            </div>
          </Dialog>
          <div style={{ height: "100vh", width: "100vw" }} className="relative">
            <ReactFlow
              nodes={nodes}
              edges={edges}
              onNodesChange={onNodesChange}
              onEdgesChange={onEdgesChange}
              onConnect={onConnect}
              nodeTypes={nodeTypes}
              fitView
              attributionPosition="top-right"
            >
              <MiniMap />
              <Controls />
              <Background />
            </ReactFlow>
          </div>
        </>
      );
    }
    
    

    myCustomNode.tsx

    import { useCallback } from "react";
    
    const handleStyle = { left: 10 };
    
    function CustomNodeTest({ data }: any) {
      const onChange = useCallback((evt: any) => {
        console.log(evt.target.value);
      }, []);
    
      return (
        <div className="p-2 rounded border border-slate-800">
          <label htmlFor="text">Custom Node with text input: </label>
          <input id="text" name="text" onChange={onChange} />
        </div>
      );
    }
    
    export default CustomNodeTest;
    
    

    Codesandbox: https://codesandbox.io/s/example-react-flow-twwxhb

    References

    Reactflow Docs

    Conclusion

    Reactflow is a powerful library that makes it easy to create interactive, node-based user interfaces. Whether you want to build a visual workflow application or a data visualization tool, Reactflow provides the tools you need to get started. With its simple API and customizable appearance, Reactflow is a great choice for any developer looking to build a node-based user interface.

    About the Author

    Related Blogs

    View All Blogs
    No blogs!

    Want #swag?

    Join our monthly raffle!

    Every month, One Lucky Duck gets free swag shipped to their doorstep, wherever in the world you are! All you have to do is join our Discord channel today and tweet about the amazing things we do. #nullcast #luckyduck

    We will announce the winners on Twitter and through our discord channel.

    Duck