DEV Community

Cássio Lacerda
Cássio Lacerda

Posted on • Updated on

How to syncing React state across multiple tabs with useState Hook and localStorage

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

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;
Enter fullscreen mode Exit fullscreen mode

Unsynchronized State

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:

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

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;
Enter fullscreen mode Exit fullscreen mode

How does it work?

Let's analyze each piece of this code to understand.

const [name, setName] = useState("");
Enter fullscreen mode Exit fullscreen mode

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

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

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;
return <input value={name} onChange={handleChange} />;
Enter fullscreen mode Exit fullscreen mode

Renders a controlled form input to get data from user.

const handleChange = (and) => {
  setName(e.target.value);
  localStorage.setItem("name", e.target.value);
};
Enter fullscreen mode Exit fullscreen mode

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.

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

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

What about Redux?

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

Top comments (5)

Collapse
 
link2twenty profile image
Andrew Bone

Interesting stuff, using local storage is so powerful.

I took this principle a little further in a post a wrote a while ago. I needed to listen internally local storage change so I made a storage hook that comes with its own listening style system. The multi-tab stuff was a happy accident I leaned into.

Here's a basic demo and here's the article I wrote about it in.

Collapse
 
cassiolacerda profile image
Cássio Lacerda

Your useLocalStorage custom hook written in Typescript is very interesting, congrats!

LocalStorage is really powerfull, but I have noticed that in cases with a lot of changes happening at the same time, the performance is poor, due to the excessive usage of JSON.parse and JSON.stringify. I started some tests with IndexedDB and maybe it can handle this scenario better than localStorage.

Collapse
 
mehmetyilmaz001 profile image
Mehmet Yılmaz

Thank you. This is a very clean solution

Collapse
 
rabbitzzc profile image
rabbitzzc

share works can do it

Collapse
 
isekaimaou1109 profile image
yoki kiriya

you also make use of redux-persist sample code to sync state across multiple tabs