DEV Community

Cover image for Mini mapping tool using react flow and zustand
Lakshmaji
Lakshmaji

Posted on

Mini mapping tool using react flow and zustand

Introduction

To create a mapping tool, we need to deal with a lot of canvas or block elements from html. React community has developed a library, reactflow as an alternative to developing flows which are based on nodes.

Demo resources

Concepts

React Flow

  • Node: A node is a block that can be dragged. A Node can be connected with other nodes. A node needs a position and a label.

  • Edge: An edge is a connection between two nodes. An edge needs a source (node id) and a target (node id).

  • Handle: A handle is a kind of port of a node that is used to connect nodes. You start a connection at a handle and end it at one another.

  • Connection Line: The connection line is the line that gets drawn while you connect two nodes with each other.

  • Transform: Used to describe the current viewport of the pane. It's an array with three numbers [x, y, zoom]

Important considerations

React Flow can be controlled or uncontrolled flow, react flow recommends using controlled flow.

The dimensions of your React Flow component depend on the parent dimensions.

zustand

Is yet another state management library and the major difference is this can be used without React.

It exposes hooks (action creators), to manage the state of your application.


Creating application

setup reactjs app

yarn create react-app canvas-react-rf --template typescript

Install React Flow & zustand

yarn add react-flow-renderer zustand

Create a application state

  • Create a state directory
  • create a file named nodes.ts within state directory
  • Create sample nodes
import { Node } from 'react-flow-renderer';

const nodes :Node[] = [
  {
    id: '1',
    type: 'input',
    data: { label: 'Input' },
    position: { x: 250, y: 25 },
  },
  {
    id: '2',
    data: { label: 'Default' },
    position: { x: 100, y: 125 },
  },
  {
    id: '3',
    type: 'output',
    data: { label: 'Output' },
    position: { x: 250, y: 250 },
  },
];

export default nodes
Enter fullscreen mode Exit fullscreen mode
  • Create a file named edges.ts within state directory
  • Create connecting lines between previously defined nodes.
import { Edge } from 'react-flow-renderer';

const edges: Edge[] =  [
  { id: 'e1-2', source: '1', target: '2' },
  { id: 'e2-3', source: '2', target: '3' },
] ;

export default edges
Enter fullscreen mode Exit fullscreen mode
  • Create application reducers and selectors using zustand
import create from "zustand";
import {
  Connection,
  Edge,
  EdgeChange,
  Node,
  NodeChange,
  addEdge,
  OnNodesChange,
  OnEdgesChange,
  OnConnect,
  applyNodeChanges,
  applyEdgeChanges,
} from "react-flow-renderer";

import initialNodes from "./nodes";
import initialEdges from "./edges";

export type NodeData = {
  color: string;
  text?: string;
};

type RFState = {
  nodes: Node[];
  edges: Edge[];
  onNodesChange: OnNodesChange;
  onEdgesChange: OnEdgesChange;
  onConnect: OnConnect;
  addNode: (node: Node<NodeData>) => void;
};

const useStore = create<RFState>((set, get) => ({
  nodes: initialNodes,
  edges: initialEdges,
  onNodesChange: (changes: NodeChange[]) => {
    set({
      nodes: applyNodeChanges(changes, get().nodes),
    });
  },
  onEdgesChange: (changes: EdgeChange[]) => {
    set({
      edges: applyEdgeChanges(changes, get().edges),
    });
  },
  onConnect: (connection: Connection) => {
    set({
      edges: addEdge(connection, get().edges),
    });
  },
  addNode(node: Node<NodeData>) {
    set({
      nodes: [...get().nodes, node],
    });
  },
}));

export default useStore;

Enter fullscreen mode Exit fullscreen mode

Consuming app state using React Flow

  • Create Wrapper components
import React from "react";
import ReactFlow from "react-flow-renderer";

import useStore from "../state/store";


const Wrapper = () => {
  const { nodes, edges, onNodesChange, onEdgesChange, onConnect } = useStore();

  return (
    <div style={{ height: "100vh" }}>
      <ReactFlow
        nodes={nodes}
        edges={edges}
        onNodesChange={onNodesChange}
        onEdgesChange={onEdgesChange}
        onConnect={onConnect}
        fitView
      />
    </div>
  );
};

export default Wrapper;
Enter fullscreen mode Exit fullscreen mode
  • Import it in App
import React from 'react';
import './App.css';
import Wrapper from './components/Wrapper';

function App() {
  return (
    <div className="App">
      <Wrapper />
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Adding a custom node to your app

  • Create a custom component
import React, { FC, useCallback } from "react";
import { Handle, Position, NodeProps } from "react-flow-renderer";
import { NodeData } from "../state/store";

const InputNode: FC<NodeProps<NodeData>> = ({ data, id }) => {
  return (
    <div style={{ background: "#9ca8b3", padding: "10px" }}>
      <Handle type="target" position={Position.Left} id={`${id}.left`} />
      <div id={id}>{data.text}</div>
      <Handle type="source" position={Position.Right} id={`${id}.right1`} />
    </div>
  );
};

export default InputNode;
Enter fullscreen mode Exit fullscreen mode
  • add them in nodeTypes for ReactFlow component
const nodeTypes: NodeTypes = {
    customInput: InputNode,
};
Enter fullscreen mode Exit fullscreen mode
  • create addNewNode function within Wrapper component
  const addNewNode = useCallback(() => {
    const newNode: Node<NodeData> = {
      id: `${getUniqueId(10)}`,
      data: { color: `red` },
      type: "customInput",
      position: {
        x: 100,
        y: 100,
      },
      style: {
        width: 150,
      },
    };
    addNode(newNode);
  }, [addNode]);
Enter fullscreen mode Exit fullscreen mode

Change our custom node to take input from user, and update app state.

  • Add a new reducer in our store.js file
  updateNode(nodeId, text) {
    set({
      nodes: get().nodes.map((node) => {
        if (node.id === nodeId) {
          return { ...node, data: { ...node.data, text } };
        }
        return node;
      }),
    });
  },
Enter fullscreen mode Exit fullscreen mode
  • Change div element into input type and add an onChange event handler
const onChange = useCallback(
    (evt: ChangeEvent<HTMLInputElement>) => {
      updateNode(id, evt.target.value);
    },
    [id, updateNode]
);

return <>
        <input
          type="text"
          onChange={onChange}
          id={id}
          style={{ width: "100%", flex: 1 }}
        />
</>
Enter fullscreen mode Exit fullscreen mode

Now you will be able to add a node and add or modify the text on it.

Notes

  • Some of the steps here are taken from reactflow.dev, you can refer to the original documentation if you need more information.

  • Source code can be found here

  • Demo

Top comments (0)