How to implement cross-tab communication

At some point in your web development journey, you might need to make your web application communicate across tabs. If haven't yet, you might wonder why would I even...

🗣️ Why would I even need to do that?

OK, it would be nice if you let me finish my first paragraph. Anyway, there are plenty of situations where cross-tab communication comes in handy. Let's explore some of them

  1. Session management - Imagine we're opening many tabs of a website. You log in on one window, and then we expect that every other window should also be logged in. The same goes for the logging out.
  2. Shared state - Our application might need to share some of the state across tabs like theming, localization, or real-time data
  3. Broadcast message - When a tab would like to broadcast some message to another tab like submitting a form or some other user actions

At this point, I believe you could see that it's really handy. And I guess you're wondering how can...

🗣️ How can I implement that?

Sigh, It's the second time you've interrupted me. But that's fine... I'll pick some common ways how to implement this with simple examples

  1. Local Storage
  2. Broadcast Channel
  3. Shared Worker

The above three ways all follow to same-origin policy, which means they work only when our application has the same origin

Local Storage

Local Storage API allows our application to store some of the data into a storage that is shared across browsers within the same origin.

This might be the simplest and most popular way.

123456789
// sender
sender.setItem('channel', 'some message');
// receiver
window.addEventListener('storage', (event) => {
if (event.key === 'channel') {
// the message is in event.newValue
}
});

It's that simple! We just need only to add the storage event listener on the receiver window. And then when the data in localstorage is changed, the handler will be invoked.

Broadcast Channel

Broadcast Channel API is one of the modern web APIs that allow communication between windows, tabs, or iframes in the same origin.

The way implemented with Broadcast Channel is quite straightforward.

123456789
const broadcastChannel = new BroadcastChannel('channel');
// sender
broadcastChannel.postMessage('some message');
// receiver
broadcastChannel.onmessage = (event) => {
// the message is in event.data
};

First, we need to create a new instance of BroadcastChannel and pass the channel name as an argument.

After that, it would be straightforward as I mentioned. I think the example code can explain itself without any explanation from me. 😆 Just kidding! we can simply use postMessage to send some messages and the receiver needs to create an event listener to onmessage.

Shared Worker

The last one, it's quite the most complex out of the three ways. I'll explain it as simple as possible.

Web Worker API allows web applications to run some script as a new background thread.

As you might already know, a web application itself runs on a single thread which means that some executions might block the UI from rendering. Web Workers can help with this!

Shared Worker is a type of Web Worker API that can be utilized by multiple windows within the same origin.

We could say that Shared Worker serves as an intermediary between our web applications, making it suitable to use as a messenger between tabs.

123456789
const sharedWorker = new SharedWorker('./worker.js');
// sender
sharedWorker.port.postMessage('some message');
// receiver
sharedWorker.port.onmessage = (event) => {
// the message is in event.data
};

As you can see the code example is very similar to the implementation from Broadcast Channel. But there's magic behind the scenes which is worker.js

worker.js
1234567891011121314151617
let ports = [];
onconnect = function(e) {
const currentPort = e.ports[0];
ports.push(currentPort);
currentPort.onmessage = function(event) {
const message = event.data;
ports.forEach(port => {
if (currentPort !== port) {
port.postMessage(message);
}
});
};
};

This is where the magic logic happens, the onconnect handler will be invoked every time any scripts under the same origin have loaded the worker.js as a Shared Worker.

We defined an array of MessagePort, which contains every MessagePort sent as a parameter from each tab. And every time the sender called postMessage, the Shared Worker will then broadcast the message to every other MessagePort from other tabs.

And that's all three ways of how we make our tabs communicate with each other. But which one...

🗣️ Which one I should use then?

😑 ← This is my face now... It depends on which feature we're implementing.

  • If we want to share some state in a storage → Local Storage
  • If we want to broadcast some messages → Broadcast Channel
  • If our application requires lots of processing → Shared Worker

However, we need to consider other conditions before deciding which way is the right choice.

Demo

I've created a simple demo that you can try out.

Demo Site
Source Code

That's it! See you next post! 👋🏼