How to syncing React state across multiple tabs with useState Hook and localStorage
Tags: react, javascript, hooks, localstorage
Published: 2021-06-18 03:54:06

With the increasing complexity of frontend applications in recent years, some challenges to maintaining user experience with the products we build are emerging all the time. It's not difficult to find users who keep multiple instances of the same application opened in more than one tab in their browsers, and synchronizing the application's state in this scenario can be tricky.

In the case of applications developed in ReactJS that work with state control using **useState** and **useContext** hooks, or even **Redux** in more complex scenarios, by default, the context is kept separately for each active tab in the user's browser.

**Unsynchronized State**

```jsx
import React, { useState } from "react";

function Unsynced() {
  const [name, setName] = useState("");

  const handleChange = (e) => {
    setName(e.target.value);
  };

  return <input value={name} onChange={handleChange} />;
}

export default Unsynced;
```

![Unsynchronized State](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lze2at1rxbsdflgprtoy.gif)

Did you know that we can synchronize the state of multiple instances of the same application in different tabs just using client-side solutions?

## Data communication between tabs

At the moment, some options for realtime data communication between multiple tabs that browsers support are:

- [Broadcast Channel API](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API);
- [Channel Messaging API](https://developer.mozilla.org/en-US/docs/Web/API/Channel_Messaging_API);
- [Window.postMessage](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage);
- [Window: storage event](https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event);

## Simple usage with useState hook

In this first example, we are going to use the `Window: storage event` feature for its simplicity, however in a real project where your application has a large data flow being synchronized, **since Storage works in synchronous way, it maybe cause UI blocks**. This way, adapt the example with one of alternatives showed above.

**Synchronized State**

```jsx
import React, { useEffect, useState } from "react";

function SyncLocalStorage() {
  const [name, setName] = useState("");

  const onStorageUpdate = (e) => {
    const { key, newValue } = e;
    if (key === "name") {
      setName(newValue);
    }
  };

  const handleChange = (e) => {
    setName(e.target.value);
    localStorage.setItem("name", e.target.value);
  };

  useEffect(() => {
    setName(localStorage.getItem("name") || "");
    window.addEventListener("storage", onStorageUpdate);
    return () => {
      window.removeEventListener("storage", onStorageUpdate);
    };
  }, []);

  return <input value={name} onChange={handleChange} />;
}

export default SyncLocalStorage;
```

## How does it work?
Let's analyze each piece of this code to understand.

```jsx
const [name, setName] = useState("");
```

We initially register `name` as a component state variable using the `useState` hook.

```jsx
useEffect(() => {
  setName(localStorage.getItem("name") || "");
  window.addEventListener("storage", onStorageUpdate);
  return () => {
    window.removeEventListener("storage", onStorageUpdate);
  };
}, []);
```

When the component is mounted:

- Checks if there is already an existing value for the `name` item in storage. If true, assign that value to the state variable `name`, otherwise, keep its value as an empty string;
- Register an event to listen for changes in storage. To improve performance, unregister the same event when the component unmounted;

```jsx
return <input value={name} onChange={handleChange} />;
```

Renders a controlled form input to get data from user.

```jsx
const handleChange = (and) => {
  setName(e.target.value);
  localStorage.setItem("name", e.target.value);
};
```

When the value of the controlled form input is modified by the user, its new value is used to update the state variable and also the storage.

```jsx
const onStorageUpdate = (e) => {
  const { key, newValue } = e;
  if (key === "name") {
    setName(newValue);
  }
};
```

When the storage is updated by one of the instances of your application opened in browser tabs, the `window.addEventListener("storage", onStorageUpdate);` is triggered and the new value is used to update state variable in all instances tabs. **Important to know that this event is not triggered for the tab which performs the storage set action**.

And the magic happens... 

![Synchronized State](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rcrtu08v2h0xu87ki5gf.gif)

## What about Redux?

In the next post in the series, let's work with Redux state in a more complex scenario.