How to syncing React state across multiple tabs with useState Hook and localStorage
Cássio Lacerda
Posted on June 18, 2021
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;
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;
How does it work?
Let's analyze each piece of this code to understand.
const [name, setName] = useState("");
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);
};
}, []);
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 variablename
, 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} />;
Renders a controlled form input to get data from user.
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.
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...
What about Redux?
In the next post in the series, let's work with Redux state in a more complex scenario.
Posted on June 18, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.