Last Updated: March 07, 2016
·
10.82K
· hasenj

Cross-tab communication using localStorage

There might be cases where you need to communicate something (perhaps sync some state?) between multiple open tabs (in the same browser) of your application.

This is not a very common situation, but there's a way to it.

Explanation

When a value is written to localStorage key, a storage event gets fired in all tabs (that are open on the same domain) except the one that did the writing. This can be exploited to communicate messages across tabs.

localStorage is a key-value store. It makes sense to reserve some key name (for example: "crossTabRelay") and use it exclusively for communicating messages across tabs.

localStorage can only store strings. I want to be able to relay more complicated messages, so I think we should use the built-in json (de)seralizer. Before we write anything, we convert it to a json string using JSON.stringify, and when we read the message, we convert it back to an object using JSON.parse.

This would allow us to send any object as a message, as long as it can be converted to json.

So with this, it's quite simple: to send a message, we turn it into a json string, then write it to localStorage at a specific key. To read sent messages from other tabs, we listen for the storage event, and if it's fired for our specific key, then it's a cross-tab message.

Note that this is a hack: I don't think that localStorage was designed with this use-case in mind.

One caveat is this: if you write a value to a localStorage key that happens to be exactly the same as the existing value (string) for that key in localStorage, no event will be fired, since there was no change made!

This won't do for me. I want to be able to send the same message multiple times in a row!

To get around this, We wrap each message in a "relay" object and put a timestamp on it, along with the actual message. When we receive the relay, we ignore the timestamp and just extract the message from it.

Finally, I want to expose a simpler API over this. When I use the crosstab functionality, I don't want to think about localStorage or storage events or anything like that. To this end I think it's useful to utilize a pub/sub library. In this example I'm using Durandal's events module (docs)

I'm also defining this as a requirejs module.

This is the API we will expose:

To send messages:

crosstab.relay(message)

To listen for messages:

var listener = crosstab.listen(function(message) {
    // ... do something with message ...
});

To turn off a particular listener:

listener.off();

Implementation

define('crosstab', function(require) {
    var Events = require('durandal/events');

    var crosstab = {}
    Events.includeIn(crosstab);

    var key = "crossTabRelay";

    // listen to storage events
    window.addEventListener('storage', function(e) {
        if(e.key == key) {
            var relay = JSON.parse(e.newValue);
            crosstab.trigger("relay", relay.message);
        }
    });

    // send a message to all open tabs; including this tab
    crosstab.relay = function(message) {
        // wrap the message in a relay object
        var relay = {
            message: message,
            timestamp: Date.now()
        }

        try {
            localStorage[key] = JSON.stringify(relay);
        } catch(e) {
            console.error("Tried to cross-tab relay non-json-able message:", message);
            throw e;
        }

        // fire the event locally
        crosstab.trigger("relay", message);
    }

    crosstab.listen = function(callback) {
        return crosstab.on("relay").then(callback);
    }

    return crosstab;
});

Note that this only works for modern browsers. IE has tons of quirks, specially IE8. There's a way to make this work for IE (and even IE8, to some extent), but it's too damn complicated, and I want to keep this simple.

Example usage

To test this without setting up a requirejs-based project with durandal and everything, just find a webapp that already uses durandal, and open two tabs of it, and open the dev console in both tabs. For example: http://ninjamock.com/

In both tabs, copy/paste the code above from inside the define block, excluding the last return statement.

Then, in the first tab, listen for messages:

crosstab.listen(function(message) {
    console.log("Received cross-tab message:", message);
})

In the second tab, send a message:

crosstab.relay("Hello earth, from outer space");

Now go back to the first tab. You should see in the console:

Received cross-tab message: Hello earth, from outer space 

So there you go!

1 Response
Add your response

We had a similar problem accesing different domains on an iframe while creating a newer tool on a cms. We had to use the HTML5 postMessage API between the old server pages (included into the iframe) and the new one ..
https://developer.mozilla.org/en-US/docs/Web/API/Window.postMessage

I'm guessing you didn't need to 'know' if the other tab was open or not, right?
You just listen to the event and fire up functions from there .. nice trick!

over 1 year ago ·