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.
Top comments (5)
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.
React: Custom hook for accessing storage
Andrew Bone ・ Apr 21 '21 ・ 8 min read
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.
Thank you. This is a very clean solution
share works can do it
you also make use of redux-persist sample code to sync state across multiple tabs