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

cassiolacerda

Cássio Lacerda

Posted on June 18, 2021

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.

💖 💪 🙅 🚩
cassiolacerda
Cássio Lacerda

Posted on June 18, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related