Photo by Juan Davila on Unsplash

Custom React Hook to Share State Between Browser’s Windows

Mostafa Darehzereshki
JavaScript in Plain English
5 min readNov 20, 2020

--

Sometimes on a Frontend App, we need to communicate between multiple tabs on the same domain which are open on the browser. A good example of this use case is an e-commerce website. Let’s say you open multiple tabs to review different products and as you’re reviewing you add those to your cart. What you expect as the use is to see your cart is updated on any tabs you go!

Here is a simple demo example I created. Open this sample App on two tabs and add items to your cart! As you can see the state of the App is shared between the browser’s tabs, but how can we do that?

A little bit of background: Cross window communication

If you want to communicate between multiple windows on the same domain there are different ways to do that. Some of the common ways of doing this communication are Window PostMessage API , Broadcast Channel API and communication through LocalStorage. I’m not gonna cover the first two ways of communication in this post but there are many examples for these two approaches which you can find online and in the documentation. I’ll focus on communicating using Localstorage because we want to persist our state as well so the next time we visit the page we still have access to the state.

Local Storage

Local storage is one of the storage available on browsers. You can simply add items and read from the storage by using a very straightforward API it provides. Here is a simple example:

localStorage.setItem('name','mostafa')console.log(localStorage.getItem('name'))
// mostafa

As you can see the API is very easy to use and you pass a key/value to localStorage.setItem and to get the value you call thelocalStorage.getItem with the key.
But also there is one more thing about localStorage that we’re gonna use in our use case. You can listen to all the changes in localStorage:

window.addEventListner('storage',(e)=>{// Something has changed on localStorage 
})

The event received onstorage has a couple of properties that two of them are what we need: key and newValue. Basically what we want to do is on each tab when we set the React state we also persist it in localStorag so the other tabs can listen to this change and update their own states.

A Small example before the final solution

Here is a small React component that we want to share the state between different tabs for this App:

import React, { useState, useEffect } from "react";function HelloStorage() {
const [name, setName] = useState("");
useEffect(() => {
localStorage.setItem("name", name);
}, [name]);
useEffect(() => {
const onReceiveMessage = (e) => {
const { key, newValue } = e;
if (key === "name") {
setName(newValue);
}
};
window.addEventListener("storage", onReceiveMessage);
return () => {
window.removeEventListener("storage", onReceiveMessage);
};
}, []);
const handleChange = (e) => {
setName(e.target.value);
};
return <input value={name} onChange={handleChange} />;
}

In the code above, you can see there are two useEffect, the first one listens to changes in our state and if there is a change it sets the state on localStorage.
There is a second useEfffect which adds an event-listener for storage and every time that there is a change on the storage it checks the changed key and if it’s our state then it updates the state inside the component. (Basically, it means another tab updated this state)

The first question that comes up is are we gonna get into a loop here? 🤔 I
f we update the state with every localStorage change it seems an infinite loop for the tab which is changing the state(updated state — -> update local storage — ->update state)!
The answer is No! We don’t get into a loop. Because the storage events don’t fire an event on the origin of the even. In other words, the tab which is changing the state doesn’t get storage event, so phew 👏

If you run this code as it is, you see a problem because every time you open a new tab the new state is empty and guess what! it sets the state to empty string across the tabs.
We’re not gonna solve that problem in this small sample code but in our final solution, we’ll address that.

A custom React Hook to share state between browser tabs

In this section, I assume you’re familiar with React Hooks and how to build custom React hooks. If you’d like to learn more about it I recommend taking a look at the Hooks section on React documents.

First, let me put the final version here and then we go over it together:

function useCrossTabState(stateKey,defaultValue){
const [state,setState] = useState(defaultValue)
const isNewSession = useRef(true)
useEffect(()=>{
if(isNewSession.current){
const currentState = localStorage.getItem(stateKey)
if(currentState){
setState(JSON.parse(currentState))
}else{
setState(defaultValue)
}
isNewSession.current=false
return
}
try{
localStorage.setItem(stateKey,JSON.stringify(state))
}catch(error){}
},[state,stateKey,defaultValue])
useEffect(()=>{
const onReceieveMessage = (e) => {
const {key,newValue} = e
if(key===stateKey){
setState(JSON.parse(newValue))
}
}
window.addEventListener('storage',onReceieveMessage)
return () => window.removeEventListener('storage',onReceieveMessage)
},[stateKey,setState])
return [state,setState]
}

What I like about React hooks is how easily you can build some custom hooks to use across your App to have a more modular and cleaner code.

This custom hook accepts two inputs: stateKey and a defaultValue . Basically, the stateKey is the key we want to use when we call localStorage.setItem and the defaultValue is the default value for the state if there is no value set on localStorage yet.

Let’s skip the isNewSession block for now and let’s take a look at first useEffect:

useEffect(()=>{
try{
localStorage.setItem(stateKey,JSON.stringify(state))
}catch(error){}
},[state,stateKey])

In this part same as the previous example we’re setting the state on localStorage every time the state changes.
Now we need to listen to the storage changes and if the change is related to our state we need to update our state:

useEffect(()=>{
const onReceieveMessage = (e) => {
const {key,newValue} = e
if(key===stateKey){
setState(JSON.parse(newValue))
}
}
window.addEventListener('storage',onReceieveMessage)
return () => window.removeEventListener('storage',onReceieveMessage)
},[stateKey,setState])

So far this is similar to what we have done in the first example except it’s more flexible and we can use it for different states across our application.

Now let’s talk about what isNewSession block does? If you remember from the first example I mentioned the problem with this approach is if you open a new tab the new tab has the default state(which in our case was an empty string) and it updates the storage with that default state and it’s gonna override the state that’s been shared with tabs.
To solve this problem we need to check and see if it’s a new session and the component is just being rendered and if that’s the case we just skip setting the state on localStorage .

if(isNewSession.current){
const currentState = localStorage.getItem(stateKey)
if(currentState){
setState(JSON.parse(currentState))
}else{
setState(defaultValue)
}
isNewSession.current=false
return
}

Now we can re-write our first example using our custom hook:

import React from "react";
import { useCrossTabState } from "./hooks";
function App() {
const [name, setName] = useCrossTabState("name", "");
const handleChange = (e) => {
setName(e.target.value);
};
return <input value={name} onChange={handleChange} />;
}
export default App;

Just keep in mind the state keys should be unique across the domain to avoid any overriding.

You can see another example of using this hook here on my GitHub: https://github.com/mostafa-drz/react-cross-windows-state

I hope this custom hook helps you in your projects and you’ve enjoyed this post. Please don’t hesitate to share your opinion with me about how you’d improve this custom hook or if you have any thoughts on it.🍻

Mostafa

--

--