ponyfoo.com

Cross-tab Communication

The upcoming SharedWorker API allows to transmit data across iframes and even browser tabs or windows. It landed in Chrome years ago, and not so long ago in Firefox, but it’s nowhere to be seen in IE or Safari. A wildly supported alternative exists that can be used today, but it’s largely unknown. Let’s explore it!

I wanted an elegant solution to the following scenario: suppose a human walks into your website, logs in, opens a second tab, and logs out in that tab. He’s still “logged in” on the first tab, except anything he touches will either redirect them to the login page or straight blow up in their face. A more inviting alternative would be to figure out that they’re logged out and do something about it, such as display a dialog asking them to re-authenticate, or maybe the login view itself.

You could use the WebSocket API for this, but that’d be overkill. I wanted a lower-level technology flyswatter, so I started looking for cross-tab communication options. The first option that popped up was using cookies or localStorage, and then periodically checking whether they were logged in or not via setInterval. I wasn’t satisfied with that answer because it would waste too many CPU cycles checking for something that might not ever come up. At that point I would’ve rather used a “comet” (also known as long-polling), Server-Sent Events, or WebSockets.

I was surprised to see that the answer was lying in front of my nose, it was localStorage all along!

Did you know that localStorage fires an event? More specifically, it fires an event whenever an item is added, modified, or removed in another browsing context. Effectively, this means that whenever you touch localStorage in any given tab, all other tabs can learn about it by listening for the storage event on the window object, like so:

window.addEventListener('storage', function (event) {
  console.log(event.key, event.newValue);
});

The event object contains a few relevant properties.

Property Description
key The affected key in localStorage
newValue The value that is currently assigned to that key
oldValue The value before modification
url The URL of the page where the change occurred

Whenever a tab modifies something in localStorage, an event fires in every other tab. This means we’re able to communicate across browser tabs simply by setting values on localStorage. Consider the following pseudo_ish_-code example:

var loggedOn;

// TODO: call when logged-in user changes or logs out
logonChanged();

window.addEventListener('storage', updateLogon);
window.addEventListener('focus', checkLogon);

function getUsernameOrNull () {
  // TODO: return whether the user is logged on
}

function logonChanged () {
  var uname = getUsernameOrNull();
  loggedOn = uname;
  localStorage.setItem('logged-on', uname);
}

function updateLogon (event) {
  if (event.key === 'logged-on') {
    loggedOn = event.newValue;
  }
}

function checkLogon () {
  var uname = getUsernameOrNull();
  if (uname !== loggedOn) {
    location.reload();
  }
}

The basic idea is that when a user has two open tabs, logs out from one of them, and goes back to the other tab, the page is reloaded and (hopefully) the server-side logic redirects them to somewhere else. The check is being done only when the tab is focused as a nod to the fact that maybe they log out and they log back in immediately, and in those cases we wouldn’t want to log them out of every other tab.

We could certainly improve that piece of code, but it serves its purpose pretty well. A better implementation would probably ask them to log in on the spot, but note that this also works the other way around: when they log in and go to another tab that was also logged out, the snippet detects that change reloading the page, and then the server would redirect them to the logged-in fountain-of-youth blessing of an experience you call your website (again, hopefully).

A simpler API

The localStorage API is arguably one of the easiest to use APIs there are, when it comes to web browsers, and it also enjoys quite thorough cross-browser support. There are, however, some quirks such as incognito Safari throwing on sets with a QuotaExceededError, no support for JSON out the box, or older browsers bumming you out.

For those reasons, I put together local-storage which is a module that provides a simplified API to localStorage, gets rid of those quirks, falls back to an in-memory store when the localStorage API is missing, and also makes it easier to consume storage events, by letting you register and unregister listeners for specific keys.

API endpoints in [email protected] (latest, at the time of this writing) are listed below.

  • ls(key, value?) gets or sets key
  • ls.get(key) gets the value in key
  • ls.set(key, value) sets key to value
  • ls.remove(key) removes key
  • ls.on(key, fn(value, old, url)) listens for changes to key in other tabs, triggers fn
  • ls.off(key, fn) unregisters listener previously added with ls.on

It’s also worth mentioning that local-storage registers a single storage event handler and keeps track of every key you want to observe, rather than register multiple storage events.

I’d be interested to learn about other use cases for low-tech communication across tabs! Certainly sounds useful for offline-first development, particularly if we keep in mind that SharedWorker might take a while to become widely supported, and WebSockets are unreliable in offline-first scenarios.

Liked the article? Subscribe below to get an email when new articles come out! Also, follow @ponyfoo on Twitter and @ponyfoo on Facebook.
One-click unsubscribe, anytime. Learn more.

Comments (22)

bhlpo wrote

Clarification: Not all open tabs but just open tabs on a given domain?

Nicolas Bevacqua wrote

All open tabs that share access to the same storage area. A storage area is either localStorage or sessionStorage for a particular domain.

Otherwise it’d be a glaring security hole, storage areas belong to a single domain. Much like cookies or session.

Even though I didn’t mention it in the article, sessionStorage also enjoys the benefits of the storage event.

Dennis Cheung wrote

sessionStorage is not that great compare to localStorage.

sessionStorage, however, does not always share same session for tabs in all browsers, even for same URL. They might share same session, if they share window.opener. But if you close/reload/reopen/enter URL, that is another story.

Ben Bankes wrote

Of course, if what you are looking for is cross-device synchronization, you would have to use web sockets over local storage or the service worker.

Johan wrote

Ben,

Cross device is not so easy as it sounds. You’re talking about eventual consistency there… You need something like a Cloud Type library… There is a JS implementation in the npm library called “cloudtypes” which has a nice working demo (cross device, online/offline handling, etc…)

Johan.

Ben Bankes wrote

Johan, it is not too awful hard. At least for PHP, there are some great tools like Ratchet, React, Supervisor, and Stunnel (for securing web sockets). I have a working implementation.

Since web sockets solves the problem “How do I do cross-device synchronization?”, it also solves the issue of cross-tab synchronization and cross-browser synchronization. It’s a one-stop shop solution.

Ben Bankes wrote

Johan, I see what you are saying. For offline apps, web sockets cannot solve the device/browser/tab synchronization problem immediately because they require a connection to the server to function. That would require an additional library like the one you mention. Thanks for the new info/library.

Kevin Jantzer wrote

Nice! I didn’t know you could listen to localStorage changes. Wouldn’t have thought it work across tabs either.

Islam Sharabash wrote

Yep! We use localstorage for the same purpose, cross tab login/logout notifications. I imagine you could use it to keep SPA models/collections in sync cross tab too.

Watch out for this gotcha in internet explorer though: The localstorage event is supposed to be fired in all tabs EXCEPT the one that triggered it. In IE it fires the event in the tab that the event originated: https://connect.microsoft.com/IE/feedback/details/774798/localstorage-event-fired-in-source-window

Wilfred Godfrey wrote

Using it for the same thing here. That and sharing any kind of updates I get or make in the active tab - I don’t want to have to make a request in each tab if I already know what the reply’ll be, that’s crazy.

Thanks for that little nugget of information about IE by the way. I haven’t even started testing in IE yet shudder Not something I look forward to.

Nicolas Bevacqua wrote

Actually laughed at the first comment in that thread.

I don’t see this as a bug, this should be a specs bug and a Firefox and Chrome bug

Jill Burrows wrote

A few years ago, I implemented an audio player for a musician-oriented hosting company. There were a few interesting requirements:

  • we needed to be able to store the time offset for the audio
  • we needed to synchronize the players across multiple tabs
  • we needed to be ensure only one player was playing ay any given time
  • we could not spend anytime changing server code or adding an API for storing player related state
  • we needed to support IE 6

I ended up using cookies to store the current player state and to pass messages (based on polling the cookie store). Each player created it’s own unique identifier. The first loaded or currently active player wrote it’s id to the cookie. Other players on other tabs would see this when they loaded and entered a disabled state if there was an existing active player. Enabling a player would cause any other active player to enter a disabled state once it updated it’s time offset. The newly enabled player would start playing from a brief instant before the last written time offset. The logic for all of this was implemented as a state machine.

This is a nice updated version of that tactic and has the benefit of being broadcast based.

Pawel Stefanowski wrote

If I am not wrong problem with “logged in” it is resolved by frameworks

Simon Ljungberg wrote

I wrote this implementation of the Bully algorithm that let’s your code elect a “master” window. This way you could for example have one window that is responsible for having a web socket connection. The master could then forward all socket messages to the other windows.

https://github.com/simme/browbeat

Islam Sharabash wrote

That looks kind of awesome, thanks for sharing!

Andrew Jackson wrote

You may want to use the Page Visibility API instead of window’s focus event to determine when to do the checking. It has wide support too (when including the webkit version).

Benjamin Milde wrote

Would’ve loved this to come up a few weeks earlier. Used this about two weeks ago to locally sync up a management and a score overview tab, without depending on any network.