LearnAdvanced Use

Computing Flows

For this guide we assume that you already know about the core concepts of React Flow and how to implement custom nodes.

Usually with React Flow, developers handle their data outside of React Flow by sending it somewhere else, like on a server or a database. Instead, in this guide we’ll show you how to compute data flows directly inside of React Flow. You can use this for updating a node based on connected data, or for building an app that runs entirely inside the browser.

What are we going to build?

By the end of this guide, you will build an interactive flow graph that generates a color out of three separate number input fields (red, green and blue), and determines whether white or black text would be more readable on that background color.

import { useCallback } from 'react';
import {
  ReactFlow,
  Background,
  useNodesState,
  useEdgesState,
  addEdge,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
 
import NumberInput from './NumberInput';
import ColorPreview from './ColorPreview';
import Lightness from './Lightness';
import Log from './Log';
 
const nodeTypes = {
  NumberInput,
  ColorPreview,
  Lightness,
  Log,
};
 
const initialNodes = [
  {
    type: 'NumberInput',
    id: '1',
    data: { label: 'Red', value: 255 },
    position: { x: 0, y: 0 },
  },
  {
    type: 'NumberInput',
    id: '2',
    data: { label: 'Green', value: 0 },
    position: { x: 0, y: 100 },
  },
  {
    type: 'NumberInput',
    id: '3',
    data: { label: 'Blue', value: 115 },
    position: { x: 0, y: 200 },
  },
  {
    type: 'ColorPreview',
    id: 'color',
    position: { x: 150, y: 50 },
    data: {
      label: 'Color',
      value: { r: undefined, g: undefined, b: undefined },
    },
  },
  {
    type: 'Lightness',
    id: 'lightness',
    position: { x: 350, y: 75 },
  },
  {
    id: 'log-1',
    type: 'Log',
    position: { x: 500, y: 0 },
    data: { label: 'Use black font', fontColor: 'black' },
  },
  {
    id: 'log-2',
    type: 'Log',
    position: { x: 500, y: 140 },
    data: { label: 'Use white font', fontColor: 'white' },
  },
];
 
const initialEdges = [
  {
    id: '1-color',
    source: '1',
    target: 'color',
    targetHandle: 'red',
  },
  {
    id: '2-color',
    source: '2',
    target: 'color',
    targetHandle: 'green',
  },
  {
    id: '3-color',
    source: '3',
    target: 'color',
    targetHandle: 'blue',
  },
  {
    id: 'color-lightness',
    source: 'color',
    target: 'lightness',
  },
  {
    id: 'lightness-log-1',
    source: 'lightness',
    sourceHandle: 'light',
    target: 'log-1',
  },
  {
    id: 'lightness-log-2',
    source: 'lightness',
    sourceHandle: 'dark',
    target: 'log-2',
  },
];
 
function ReactiveFlow() {
  const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
  const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
  const onConnect = useCallback(
    (params) => setEdges((eds) => addEdge(params, eds)),
    [],
  );
  return (
    <ReactFlow
      nodeTypes={nodeTypes}
      nodes={nodes}
      edges={edges}
      onNodesChange={onNodesChange}
      onEdgesChange={onEdgesChange}
      onConnect={onConnect}
      fitView
    >
      <Background />
    </ReactFlow>
  );
}
 
export default ReactiveFlow;

Creating custom nodes

Let’s start by creating a custom input node (NumberInput.js) and add three instances of it. We will be using a controlled <input type="number" /> and limit it to integer numbers between 0 - 255 inside the onChange event handler.

import { useCallback, useState } from 'react';
import { Handle, Position } from '@xyflow/react';
 
import { getStaticCode } from 'xy-shared/server';
export const getStaticProps = getStaticCode(['learn/computing-6', 'learn/computing', 'learn/computing-2', 'learn/computing-3', 'learn/computing-4', 'learn/computing-5', 'learn/computing-6']);
 
 
function NumberInput({ id, data }) {
  const [number, setNumber] = useState(0);
 
  const onChange = useCallback((evt) => {
    const cappedNumber = Math.round(
      Math.min(255, Math.max(0, evt.target.value)),
    );
    setNumber(cappedNumber);
  }, []);
 
  return (
    <div className="number-input">
      <div>{data.label}</div>
      <input
        id={`number-${id}`}
        name="number"
        type="number"
        min="0"
        max="255"
        onChange={onChange}
        className="nodrag"
        value={number}
      />
      <Handle type="source" position={Position.Right} />
    </div>
  );
}
 
export default NumberInput;

Next, we’ll add a new custom node (ColorPreview.js) with one target handle for each color channel and a background that displays the resulting color. We can use mix-blend-mode: 'difference'; to make the text color always readable.

Whenever you have multiple handles of the same kind on a single node, don’t forget to give each one a seperate id!

Let’s also add edges going from the input nodes to the color node to our initialEdges array while we are at it.

import { Handle, Position } from '@xyflow/react';
 
function ColorPreview() {
  const color = { r: 0, g: 0, b: 0 };
 
  return (
    <div
      className="node"
      style={{
        background: `rgb(${color.r}, ${color.g}, ${color.b})`,
      }}
    >
      <div>
        <Handle
          type="target"
          position={Position.Left}
          id="red"
          className="handle"
        />
        <label htmlFor="red" className="label">
          R
        </label>
      </div>
      <div>
        <Handle
          type="target"
          position={Position.Left}
          id="green"
          className="handle"
        />
        <label htmlFor="green" className="label">
          G
        </label>
      </div>
      <div>
        <Handle
          type="target"
          position={Position.Left}
          id="blue"
          className="handle"
        />
        <label htmlFor="red" className="label">
          B
        </label>
      </div>
    </div>
  );
}
 
export default ColorPreview;

Computing data

How do we get the data from the input nodes to the color node? This is a two step process that involves two hooks created for this exact purpose:

  1. Store each number input value inside the node’s data object with help of the updateNodeData callback.
  2. Find out which nodes are connected by using useHandleConnections and then use useNodesData for receiving the data from the connected nodes.

Step 1: Writing values to the data object

First let’s add some initial values for the input nodes inside the data object in our initialNodes array and use them as an initial state for the input nodes. Then we’ll grab the function updateNodeData from the useReactFlow hook and use it to update the data object of the node with a new value whenever the input changes.

By default, the data you pass to updateNodeData will be merged with the old data object. This makes it easier to do partial updates and saves you in case you forget to add {...data}. You can pass { replace: true } as an option to replace the object instead.

import { useCallback, useState } from 'react';
import { Handle, Position, useReactFlow } from '@xyflow/react';
 
function NumberInput({ id, data }) {
  const { updateNodeData } = useReactFlow();
  const [number, setNumber] = useState(data.value);
 
  const onChange = useCallback((evt) => {
    const cappedNumber = Math.min(255, Math.max(0, evt.target.value));
    setNumber(cappedNumber);
    updateNodeData(id, { value: cappedNumber });
  }, []);
 
  return (
    <div className="number-input">
      <div>{data.label}</div>
      <input
        id={`number-${id}`}
        name="number"
        type="number"
        min="0"
        max="255"
        onChange={onChange}
        className="nodrag"
        value={number}
      />
      <Handle type="source" position={Position.Right} />
    </div>
  );
}
 
export default NumberInput;
⚠️

When dealing with input fields you don’t want to use a nodes data object as UI state directly.

There is a delay in updating the data object and the cursor might jump around erraticly and lead to unwanted inputs.

Step 2: Getting data from connected nodes

We start by determining all connections for each handle with the useHandleConnections hook and then fetching the data for the first connected node with updateNodeData.

Note that each handle can have multiple nodes connected to it and you might want to restrict the number of connections to a single handle inside your application. Check out the connection limit example to see how to do that.

And there you go! Try changing the input values and see the color change in real time.

import {
  Handle,
  Position,
  useNodesData,
  useHandleConnections,
} from '@xyflow/react';
 
function ColorPreview() {
  const redConnections = useHandleConnections({
    type: 'target',
    id: 'red',
  });
  const redNodeData = useNodesData(redConnections?.[0].source);
 
  const greenConnections = useHandleConnections({
    type: 'target',
    id: 'green',
  });
  const greenNodeData = useNodesData(greenConnections?.[0].source);
 
  const blueConnections = useHandleConnections({
    type: 'target',
    id: 'blue',
  });
  const blueNodeData = useNodesData(blueConnections?.[0].source);
 
  const color = {
    r: blueNodeData?.data ? redNodeData.data.value : 0,
    g: greenNodeData?.data ? greenNodeData.data.value : 0,
    b: blueNodeData?.data ? blueNodeData.data.value : 0,
  };
 
  return (
    <div
      className="node"
      style={{
        background: `rgb(${color.r}, ${color.g}, ${color.b})`,
      }}
    >
      <div>
        <Handle
          type="target"
          position={Position.Left}
          id="red"
          className="handle"
        />
        <label htmlFor="red" className="label">
          R
        </label>
      </div>
      <div>
        <Handle
          type="target"
          position={Position.Left}
          id="green"
          className="handle"
        />
        <label htmlFor="green" className="label">
          G
        </label>
      </div>
      <div>
        <Handle
          type="target"
          position={Position.Left}
          id="blue"
          className="handle"
        />
        <label htmlFor="red" className="label">
          B
        </label>
      </div>
    </div>
  );
}
 
export default ColorPreview;

Improving the code

It might seem awkward to get the connections first, and then the data seperately for each handle. For nodes with multiple handles like these, you should consider creating a custom handle component that isolates connection states and node data binding. We can create one inline.

ColorPreview.js
// {...}
function CustomHandle({ id, label, onChange }) {
  const connections = useHandleConnections({
    type: 'target',
    id,
  });
 
  const nodeData = useNodesData(connections?.[0].source);
 
  useEffect(() => {
    onChange(nodeData?.data ? nodeData.data.value : 0);
  }, [nodeData]);
 
  return (
    <div>
      <Handle
        type="target"
        position={Position.Left}
        id={id}
        className="handle"
      />
      <label htmlFor="red" className="label">
        {label}
      </label>
    </div>
  );
}

We can promote color to local state and declare each handle like this:

ColorPreview.js
// {...}
function ColorPreview() {
  const [color, setColor] = useState({ r: 0, g: 0, b: 0 });
 
  return (
    <div
      className="node"
      style={{
        background: `rgb(${color.r}, ${color.g}, ${color.b})`,
      }}
    >
      <CustomHandle
        id="red"
        label="R"
        onChange={(value) => setColor((c) => ({ ...c, r: value }))}
      />
      <CustomHandle
        id="green"
        label="G"
        onChange={(value) => setColor((c) => ({ ...c, g: value }))}
      />
      <CustomHandle
        id="blue"
        label="B"
        onChange={(value) => setColor((c) => ({ ...c, b: value }))}
      />
    </div>
  );
}
 
export default ColorPreview;

Getting more complex

Now we have a simple example of how to pipe data through React Flow. What if we want to do something more complex, like transforming the data along the way? Or even take different paths? We can do that too!

Continuing the flow

Let’s extend our flow. Start by adding an output <Handle type="source" position={Position.Right} /> to the color node and remove the local component state.

Because there are no inputs fields on this node, we don’t need to keep a local state at all. We can just read and update the node’s data object directly.

Next, we add a new node (Lightness.js) that takes in a color object and determines if it is either a light or dark color. We can use the relative luminance formula luminance = 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b to calculate the perceived brightness of a color (0 being the darkest and 255 being the brightest). We can assume everything >= 128 is a light color.

import { useState, useEffect } from 'react';
import {
  Handle,
  Position,
  useHandleConnections,
  useNodesData,
} from '@xyflow/react';
 
function LightnessNode() {
  const connections = useHandleConnections({ type: 'target' });
  const nodesData = useNodesData(connections?.[0].source);
 
  const [lightness, setLightness] = useState('dark');
 
  useEffect(() => {
    if (nodesData?.data) {
      const color = nodesData.data.value;
      setLightness(
        0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b >= 128
          ? 'light'
          : 'dark',
      );
    } else {
      setLightness('dark');
    }
  }, [nodesData]);
 
  return (
    <div
      className="lightness-node"
      style={{
        background: lightness === 'light' ? 'white' : 'black',
        color: lightness === 'light' ? 'black' : 'white',
      }}
    >
      <Handle type="target" position={Position.Left} />
      <div>
        This color is
        <p style={{ fontWeight: 'bold', fontSize: '1.2em' }}>{lightness}</p>
      </div>
    </div>
  );
}
 
export default LightnessNode;

Conditional branching

What if we would like to take a different path in our flow based on the perceived lightness? Let’s give our lightness node two source handles light and dark and separate the node data object by source handle IDs. This is needed if you have multiple source handles to distinguish between each source handle’s data.

But what does it mean to “take a different route”? One solution would be to assume that null or undefined data hooked up to a target handle is considered a “stop”. In our case we can write the incoming color into data.values.light if it’s a light color and into data.values.dark if it’s a dark color and set the respective other value to null.

Don’t forget to add flex-direction: column; and align-items: end; to reposition the handle labels.

import { useState, useEffect } from 'react';
import {
  Handle,
  Position,
  useHandleConnections,
  useNodesData,
  useReactFlow,
} from '@xyflow/react';
 
function LightnessNode({ id }) {
  const { updateNodeData } = useReactFlow();
 
  const connections = useHandleConnections({ type: 'target' });
  const nodesData = useNodesData(connections?.[0].source);
 
  const [lightness, setLightness] = useState('dark');
 
  useEffect(() => {
    if (nodesData?.data) {
      const color = nodesData.data.value;
      const isLight =
        0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b >= 128;
      setLightness(isLight ? 'light' : 'dark');
 
      const newNodeData = isLight
        ? { light: color, dark: null }
        : { light: null, dark: color };
      updateNodeData(id, newNodeData);
    } else {
      setLightness('dark');
      updateNodeData(id, { light: null, dark: { r: 0, g: 0, b: 0 } });
    }
  }, [nodesData, updateNodeData]);
 
  return (
    <div
      className="lightness-node"
      style={{
        background: lightness === 'light' ? 'white' : 'black',
        color: lightness === 'light' ? 'black' : 'white',
      }}
    >
      <Handle type="target" position={Position.Left} />
      <p style={{ marginRight: 10 }}>Light</p>
      <Handle
        type="source"
        id="light"
        position={Position.Right}
        style={{ top: 25 }}
      />
      <p style={{ marginRight: 10 }}>Dark</p>
      <Handle
        type="source"
        id="dark"
        position={Position.Right}
        style={{ top: 75 }}
      />
    </div>
  );
}
 
export default LightnessNode;

Cool! Now we only need a last node to see if it actually works… We can create a custom debugging node (Log.js) that displays the hooked up data, and we’re done!

import { Handle, useHandleConnections, useNodesData } from '@xyflow/react';
 
function Log({ data }) {
  const connections = useHandleConnections({ type: 'target' });
 
  const nodeData = useNodesData(connections?.[0].source);
 
  const color = nodeData.data
    ? nodeData.data[connections?.[0].sourceHandle]
    : null;
 
  return (
    <div
      className="log-node"
      style={{
        background: color ? `rgb(${color.r}, ${color.g}, ${color.b})` : 'white',
        color: color ? data.fontColor : 'black',
      }}
    >
      {color ? data.label : 'Do nothing'}
      <Handle type="target" position="left" />
    </div>
  );
}
 
export default Log;

Summary

You have learned how to move data through the flow and transform it along the way. All you need to do is

  1. store data inside the node’s data object with help of updateNodeData callback.
  2. find out which nodes are connected by using useHandleConnections and then use useNodesData for receiving the data from the connected nodes.

You can implement branching for example by interpreting incoming data that is undefined as a “stop”. As a side note, most flowgraphs that also have a branching usually seperate the triggering of nodes from the actual data hooked up to the nodes. Unreal Engines Blueprints are a good example for this.

One last note before you go: you should find a consistent way of structuring all your node data, instead of mixing ideas like we did just now. This means for example, if you start working with splitting data by handle ID you should do it for all nodes, regardless whether they have multiple handles or not. Being able to make assumptions about the structure of your data throughout your flow will make life a lot easier.