Build a Custom Hook for Real-Time State Synchronization in React
Tags: react, javascript, realtime
Published: 2021-01-18 19:41:12

---
title: Build a Custom Hook for Real-Time State Synchronization in React
published: true
description: React hooks gives us a simpler way of writing stateful logic so that it can be reused and tested separately. In this article, you will learn how to find and extract common logic as a custom hook function. You will learn this by building a custom hook function for handling real-time state synchronisation.
tags: react, javascript, realtime
//cover_image: https://direct_url_to_image.jpg
---

Hooks are a new addition to React since version 16.8. They give us a simpler way to write stateful logic so that it can be reused and tested separately. It's been a while since its release and you may have used it in your apps. If you've not used it and don't know why you need it, refer to the [docs](https://reactjs.org/docs/hooks-intro.html) before you continue reading. 

Hooks allow you to reuse stateful logic without changing your component hierarchy, making it easier to share this logic across many components. The focus of this post is to show you how to extract reusable stateful logic to a custom hook and use it in any component. 

The example will be an editable table and a form where data changes will be propagated to other users of the app in real-time. In the end, you'll have a custom hook working like Redux in real-time, but with a few lines of code and integration of a real-time data synchronization service. 

If you just want to see working code, head over to [GitHub and grab the code](https://github.com/pmbanugo/realtime-custom-react-hooks). If you don't want to read the steps to set up the application and only want to see how we extract the logic as a hook function and refactor the components, then skip to the *Creating and Using Custom Hooks* section. 

## Application Set-Up

We're going to use **create-react-app** to bootstrap a new React application and also install react-table. Open your command-line application and run the commands below:

- npx create-react-app realtime-react-hooks
- cd realtime-react-hooks && npm i react-table@6

With the project created, we're going to add the components we need. Add a new file **components/Header.js** and put the code below in it.

```javascript
import React from "react";
import logo from "../logo.svg";

const Header = () => (
  <header>
    <img src={logo} className="App-logo" alt="logo" />
    <h1 className="App-title">Realtime React Datagrid</h1>
  </header>
);

export default Header;
```

Add another component **components/Form.js** and paste the code in the section below in it.

```javascript
import React from "react";

const Form = () => {
  const [firstName, setFirstName] = React.useState("");
  const [lastName, setLastName] = React.useState("");

  const handleChange = (event) => {
    if (event.target.name === "firstName") setFirstName(event.target.value);
    if (event.target.name === "lastName") setLastName(event.target.value);
  };

  const handleSubmit = (event) => {
    event.preventDefault();
  };

  return (
    <form onSubmit={handleSubmit}>
      <h3>Add new record</h3>
      <label>
        FirstName: 
        <input
          type="text"
          name="firstName"
          value={firstName}
          onChange={handleChange}
        />
      </label>
      &nbsp;&nbsp;
      <label>
        LastName: 
        <input
          type="text"
          name="lastName"
          value={lastName}
          onChange={handleChange}
        />
      </label>
      &nbsp;&nbsp;&nbsp;
      <input type="submit" value="Add" />
    </form>
  );
};

export default Form;
```

This is a simple HTML form that will be used to collect data that will be added to the table. The `handleSubmit` function does nothing for now, but we'll modify it in a later section. 

You're going to add another component that you will put in a file called **components/Table.js**. Create that file and paste the code below in it.

```javascript
import React from "react";
import ReactTable from "react-table";
import "react-table/react-table.css";

const Table = () => {
  const [data, setData] = React.useState([]);

  const renderEditableCell = (data) => (cellInfo) => {
    return (
      <div
        style={{ backgroundColor: "#fafafa" }}
        contentEditable
        suppressContentEditableWarning
        onBlur={(e) => {
          let row = data[cellInfo.index];
          row[cellInfo.column.id] = e.target.innerHTML;
          // TODO: update state with new value
        }}
        dangerouslySetInnerHTML={{
          __html: data[cellInfo.index][cellInfo.column.id],
        }}
      />
    );
  };

  return (
    <ReactTable
      data={data}
      columns={[
        {
          Header: "First Name",
          accessor: "firstName",
          Cell: renderEditableCell(data),
        },
        {
          Header: "Last Name",
          accessor: "lastName",
          Cell: renderEditableCell(data),
        },
        {
          Header: "Full Name",
          id: "full",
          accessor: (d) => (
            <div
              dangerouslySetInnerHTML={{
                __html: d.firstName + " " + d.lastName,
              }}
            />
          ),
        },
      ]}
      defaultPageSize={10}
      className="-striped -highlight"
    />
  );
};

export default Table;
```

The `Table` component renders a table with data, and it is editable. We're using `react-table` library to achieve this but I won't go into details about its API. For this example, we define the table cell headers and what data should be rendered in each cell in the `renderEditableCell` function.

## Using Hooks and Adding Real-time Data

Now that we have the base for the application, we will add a library that will be used to manage real-time data synchronization and use hooks to manage stateful logic. We will be using [Hamoni Sync](www.hamoni.tech) which allows you to store and retrieve data in real-time. It has a JavaScript package that we can install from npm. 

Run `npm install hamoni-sync` to install the package.

In order to use the service, we need to sign up to get an **Application ID** and **Account ID**. Follow the steps below to register and retrieve your account and application ID which will be needed later.

- Register and log in to Hamoni dashboard.
- Enter your preferred application name in the text field and click the create button. This should create the app and display it in the application list section.
- Expand the **Account ID** card to get your account ID.

![Hamoni Sync Dashboard](/dashboard.png "Hamoni Sync Dashboard")

You're going to use the `hamoni-sync` library to retrieve data and get real-time updates. You’re going to create a context object that’ll be used to pass the instance of `hamoni-sync` to components that need it. Add a new file called **HamoniContext.js** with the code below:

```javascript
import React from "react";

const HamoniContext = React.createContext({});
export default HamoniContext;
```

Open **App.js** and update it with the code below.

```javascript
import React, { useState, useEffect } from "react";
import "./App.css";
import Hamoni from "hamoni-sync";

import Header from "./components/Header";
import Table from "./components/Table";
import Form from "./components/Form";

import HamoniContext from "./HamoniContext";

const App = () => {
  const accountId = "REPLACE_WITH_ACCOUNT_ID";
  const appId = "REPLACE_WITH_APP_ID";
  const [hamoni, setHamoni] = useState();

  useEffect(() => {
    const initialiseHamoniSync = async () => {
      // recommended to generate this from your backend and send to your client apps.
      const response = await fetch("https://api.sync.hamoni.tech/v1/token", {
        method: "POST",
        headers: {
          "Content-Type": "application/json; charset=utf-8",
        },
        body: JSON.stringify({ accountId, appId }),
      });

      const token = await response.json();
      const hamoniSync = new Hamoni(token);
      await hamoniSync.connect();

      setHamoni(hamoniSync);
    };

    initialiseHamoniSync();
  }, [accountId, appId]);

  return (
    <HamoniContext.Provider value={hamoni}>
      <div className="App">
        <Header />
        <Form />
        <br />
        <Table />
      </div>
    </HamoniContext.Provider>
  );
};

export default App;
```

In the code you just added, we initialize the Hamoni Sync client and pass it to the `Form` and `Table` components using the context API. In order to connect to Hamoni Sync server, you need an authentication token. This is generated from a specific API as you can see. Although we put that in the React code, it is recommended to do this on the backend and send the token to your React client. This is to avoid exposing your account and application ID. 

For the `Form` component, we want to store the data collected and display it in the table. We're going to update the component's logic to include logic for this. To do this, import the `HamoniContext` from **App.js** and get access to the instance of Hamoni Sync: 

```javascript
import HamoniContext from "../HamoniContext";

const Form = () => {
  ....existing code

  const hamoni = React.useContext(HamoniContext);
  const [syncPrimitive, setSyncPrimitive] = React.useState(null);

  React.useEffect(() => {
    if (hamoni) {
      const getState = async () => {
        try {
          const listPrimitive = await hamoni.get("datagrid");
          setSyncPrimitive(listPrimitive);
        } catch (error) {
          console.log(("Hamoni Sync Error", error));
        }
      };
      getState();
    }
  }, [hamoni]);

  const handleSubmit = (event) => {
    event.preventDefault();

    syncPrimitive.add({
      firstName: firstName,
      lastName: lastName,
    });
    setLastName("");
    setFirstName("");
  };

  ...existing code
}
```

Hamoni Sync has what is called Sync primitives as a way to store and modify state. There are three kinds of Sync primitives: Value, Object, and List primitives. We’re going to use List primitive because it provides an API for us to store and modify data that needs to be stored in an array-like manner. You can read more about sync primitives from the [docs](https://docs.hamoni.tech/introduction-to-sync-primitives.html). 

Just as you name variables to hold your data, you define names for the container where your data is stored. This is the name that is used to retrieve the data. In our example, we used `hamoni.get("datagrid")` to retrieve the state object, and `datagrid` is the name of the state. In the `handleSubmit` function, we update the state by calling `syncPrimitive.add()`. 

We want to disable the `Add` button until Hamoni Sync is ready. Therefore we will update line 24 to:

```html
<input type="submit" value="Add" disabled={syncPrimitive === null} />
```

Let's update the `Table` component to retrieve data from Hamoni Sync. First, import the `HamoniContext`:

```javascript
import HamoniContext from "../HamoniContext";
```

Then add the code below to the component's function starting from line 8.

```javascript
  const hamoni = React.useContext(HamoniContext);
  const [syncPrimitive, setSyncPrimitive] = React.useState(null);

  React.useEffect(() => {
    if (hamoni) {
      const getState = async () => {
        try {
          const listPrimitive = await hamoni.get("datagrid");
          setSyncPrimitive(listPrimitive);
        } catch (error) {
          console.log(("Hamoni Sync Error: ", error));
        }
      };
      getState();
    }
  }, [hamoni]);

  React.useEffect(() => {
    if (syncPrimitive) {
      setData([...syncPrimitive.getAll()]);

      syncPrimitive.onSync((data) => {
        setData([...data]);
      });

      syncPrimitive.onItemUpdated((item) => {
        setData((previousData) => [
          ...previousData.slice(0, item.index),
          item.value,
          ...previousData.slice(item.index + 1),
        ]);
      });

      syncPrimitive.onItemAdded((item) => {
        setData((previousData) => [...previousData, item.value]);
      });
    }
  }, [syncPrimitive]);
```

We added two *useEffect* logics. The first is to get an object that will be used to retrieve data stored in Hamoni Sync, and the second one gets data and updates the React state, then subscribes to receive changes made to the data. If you want to learn more about Hamoni Sync's API, please refer to the [docs](https://docs.hamoni.tech/list-primitive.html). 

Replace the comment on line 56 in **Table.js** with the code statement below:

```
syncPrimitive.update(cellInfo.index, row);
```

This code is used to update the state in Hamoni Sync, which then gets propagated to connected clients. 

## Creating and Using Custom Hooks

Now, we have code to connect to Hamoni Sync and work with the data using the JavaScript library. We can add and update data in real-time. You may have noticed that the *Form* and *Table* components share a similar logic for retrieving Hamoni Sync's state primitive. We can extract this logic into a custom hook that can be used in those components without duplicating code. 

A custom hook is a function whose name starts with ”use” and may call other hook functions. It doesn't need to have any specific argument or return type.

How do we extract a custom hook from our current codebase?

If you look at *Table* and *Form* components, we have this exact statement in both of them.

```javascript
  React.useEffect(() => {
    if (hamoni) {
      const getState = async () => {
        try {
          const listPrimitive = await hamoni.get("datagrid");
          setSyncPrimitive(listPrimitive);
        } catch (error) {
          console.log(("Hamoni Sync Error: ", error));
        }
      };
      getState();
    }
  }, [hamoni]);
```

We will extract this logic into a separate function which we will call `useSyncState` and it'll return the `listPrimitive` object. 

Create a new folder named **hooks** with a file called **use-sync.js** and paste the code below in it.

```javascript
import { useState, useContext, useEffect } from "react";
import HamoniContext from "../HamoniContext";

function useSyncState(name) {
  const hamoni = useContext(HamoniContext);
  const [syncPrimitive, setSyncPrimitive] = useState(null);

  useEffect(() => {
    if (hamoni) {
      const getState = async () => {
        try {
          const listPrimitive = await hamoni.get(name);
          setSyncPrimitive(listPrimitive);
        } catch (error) {
          console.log(("Hamoni Sync Error", error));
        }
      };
      getState();
    }
  }, [hamoni, name]);

  return syncPrimitive;
}

export default useSyncState;
```

There you have it! A custom hook that returns an object that will be used to access state in Hamoni Sync. In order to use it, we'll replace the code on line 2 of **Form.js**, and line 4 of **Table.js** with:

```javascript
import useSyncState from "../hooks/use-sync"
```

In **Form.js** & **Table.js**, replace line 8 to 23 where you have the `useContext` and `useEffect` hooks with the code below. 

```javascript
const syncPrimitive = useSyncState("datagrid");
```

Now you have less code in the component and can reuse the hook’s logic in any component. With the combination of hooks and Hamoni Sync, we have some sort of real-time Redux logic where there's a single source of truth that gets updated in real-time. 

We can also extract the code in **App.js** to use a custom hook. In *App.js** we have this code in it:

```javascript
 const [hamoni, setHamoni] = useState();

useEffect(() => {
  const initialiseHamoniSync = async () => {
    // recommended to generate this from your backend and send it to your client apps.
    const response = await fetch("https://api.sync.hamoni.tech/v1/token", {
      method: "POST",
      headers: {
        "Content-Type": "application/json; charset=utf-8",
      },
      body: JSON.stringify({ accountId, appId }),
    });

    const token = await response.json();
    const hamoniSync = new Hamoni(token);
    await hamoniSync.connect();

    setHamoni(hamoniSync);
  };

  initialiseHamoniSync();
}, [accountId, appId]);
```

We can extract this piece into a separate custom hook. To do that, create new file **use-hamoni.js** in the *hooks* folder. Copy the code below and paste in it.

```javascript
 import { useState, useEffect } from "react";
import Hamoni from "hamoni-sync";

const useHamoni = (accountId, appId) => {
  const [hamoni, setHamoni] = useState();

  useEffect(() => {
    const initialiseHamoniSync = async () => {
      // recommended to generate this from your backend and send it to your client apps.
      const response = await fetch("https://api.sync.hamoni.tech/v1/token", {
        method: "POST",
        headers: {
          "Content-Type": "application/json; charset=utf-8",
        },
        body: JSON.stringify({ accountId, appId }),
      });

      const token = await response.json();
      const hamoniSync = new Hamoni(token);
      await hamoniSync.connect();

      setHamoni(hamoniSync);
    };

    initialiseHamoniSync();
  }, [accountId, appId]);

  return hamoni;
};

export default useHamoni;
```

We can then use this import and use this hook in **App.js**. In **App.js**, remove the import statement for `hamoni-sync` and after line 8 add the import statement to the `useHamoni` hook.

```javascript
import useHamoni from "./hooks/use-hamoni";
```

Then replace the `useState` and `useEffect` statements with the code statement below.

```javascript
const hamoni = useHamoni(accountId, appId);
```

Now your **App.js** file should have the same content as the code below.

```javascript
import React from "react";
import "./App.css";

import Header from "./components/Header";
import Table from "./components/Table";
import Form from "./components/Form";

import HamoniContext from "./HamoniContext";
import useHamoni from "./hooks/use-hamoni";

const App = () => {
  const accountId = "REPLACE_WITH_ACCOUNT_ID";
  const appId = "REPLACE_WITH_APP_ID";
  const hamoni = useHamoni(accountId, appId);

  return (
    <HamoniContext.Provider value={hamoni}>
      <div className="App">
        <Header />
        <Form />
        <br />
        <Table />
      </div>
    </HamoniContext.Provider>
  );
};

export default App;
```

Now you have two custom hooks in the application and you can use this strategy in extracting logic as custom hook functions in your application.

In order to run the application and get data without errors, we need to create/initialize the state in Hamoni Sync. This is according to Hamoni Sync's [design](https://docs.hamoni.tech/list-primitive.html). For this reason, you will add a script that you can run once to set up the service with data. Add a new file **seed.js** and paste the code below in it:

```javascript
const Hamoni = require("hamoni-sync");
const hamoni = new Hamoni("AccountID", "APP_ID");

hamoni
  .connect()
  .then(response => {
    hamoni
      .createList("datagrid", [
        { firstName: "James", lastName: "Darwin" },
        { firstName: "Jimmy", lastName: "August" }
      ])
      .then(() => console.log("create success"))
      .catch(error => console.log(error));
  })
  .catch(error => console.log(error));
```

Replace the `AccountID` and `APP_ID` placeholders with your account details. Then run this script from the command line by running the command `node seed.js`. After this is done you can run the React app with `npm start` and try out the application. 

![Custom React Hooks](https://dev-to-uploads.s3.amazonaws.com/i/tuevk1ybt20k5p4qsggq.gif)

You can find source code for this example on [GitHub](https://github.com/pmbanugo/realtime-custom-react-hooks).


> Originally posted on [Telerik Blog](https://www.telerik.com/blogs/build-custom-hook-real-time-state-synchronization-react)