Mini mapping tool using react flow and zustand
Tags: react, reactflow, nodeui
Published: 2022-06-08 15:53:24

## 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](https://reactflow.dev) as an alternative to developing flows which are based on nodes.

[Demo resources](#notes)

### 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j2ukds749twe9n0beyft.png)

 
#### Install React Flow & zustand


![yarn add react-flow-renderer zustand](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kq1sa8kx92ncum3h9ojm.png)
  
#### Create a application state

- Create a **state** directory
- create a file named `nodes.ts` within **state** directory
- Create sample nodes

```tsx
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
```  

- Create a file named `edges.ts` within **state** directory
- Create connecting lines between previously defined nodes.

```tsx
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
```
 
- Create application reducers and selectors using zustand

```tsx
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;

```

 

#### Consuming app state using React Flow

- Create **Wrapper** components

```tsx
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;
```

- Import it in `App`

```tsx
import React from 'react';
import './App.css';
import Wrapper from './components/Wrapper';

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

export default App;
``` 

#### Adding a custom node to your app

- Create a custom component
```tsx
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;
```

- add them in nodeTypes for `ReactFlow` component

```tsx
const nodeTypes: NodeTypes = {
    customInput: InputNode,
};
```
- create `addNewNode` function within `Wrapper` component
```tsx
  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]);
```

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

- Add a new reducer in our `store.js` file 
```tsx
  updateNode(nodeId, text) {
    set({
      nodes: get().nodes.map((node) => {
        if (node.id === nodeId) {
          return { ...node, data: { ...node.data, text } };
        }
        return node;
      }),
    });
  },
```
- Change `div` element into `input` type and add an `onChange` event handler

```tsx
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 }}
        />
</>
``` 

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](https://reactflow.dev/docs/guides/state-management/), you can refer to the original documentation if you need more information.

- Source code can be found [here](https://github.com/lakshmaji/mind-mapping-tool)
- [Demo](https://lakshmaji.github.io/mind-mapping-tool/)