Unread Notifications

Latest Notifications

No Notifications

You're all set! Start a discussion by leaving a comment on a lesson or replying to an existing comment.

Cross-Tab Communication

How To Do Cross-Tab Communication In JavaScript With LocalStorage

8 MIN READ
2 MONTHS AGO

In this lesson, we'll be going over how to do cross-tab communication using LocalStorage. Of the three methods we'll be discussing, this one is the most browser compatible method; despite it being a workaround.

Watch on YouTube

What We'll Be Building

In each lesson in this series we'll be building the same messaging application. You'll be able to post a message from one tab and receive it in another. Messages sent from one tab will appear to the right and will be blue. Messages received from other tabs will be gray and to the left, similar to texting.

You can checkout the end-result of today's lesson here:

https://cross-tab.surge.sh/localstorage/

Following Along?

If you'd like to follow along, I have a repository set up with where we'll be starting in this lesson. It's got the project structure and boilerplate markup for our page so we don't have to waste time adding that in. So, go ahead and clone it down, install the dependency (serve), and run npm run serve to get the dev server started up.

https://github.com/jagr-co/cross-tab-communication/tree/00_StartHere

In this lesson, we'll be working within pages -> localstorage -> index.html.

Cross-Tab Communication

Let's start with why you're here, cross-tab communication. We can use LocalStorage for this because it's shared across browser contexts for the same origin and because we can listen for changes. To start with Let's add our LocalStorage event listener and a handler method that will be called when LocalStorage changes.

window.addEventListener('storage', handleNewMessage);

function handleNewMessage(event) {

}

That's all we need to actually communicate across all our tabs. The main caveat here is this listener will not be called on the tab where the change has occurred. Within our event, there are three main properties we care about key, newValue, and oldValue. Key is the SessionStorage key change changed. Then old and new value are pretty self-explanatory.

Binding Our Forms

On our page, we have two forms, one for sending a message and another for clearing all messages. To start out, let's get submit handlers bound to these two forms along with their handler functions. We're also going to have a few global variables to this functionality holding some elements and storage keys.

const messagesEl = document.getElementById('messages');
const messagesPlaceholderEl = document.getElementById('messagesPlaceholder');
const messagesKey = 'messages';
const senderIdKey = 'senderId';

document.forms.sendMessageForm.addEventListener('submit', handleSendMessage);
document.forms.clearMessagesForm.addEventListener('submit', handleClearMessages);
window.addEventListener('storage', handleNewMessage);

function handleSendMessage(event) {
  event.preventDefault();
}

function handleClearMessages(event) {
  if (event) event.preventDefault();
}

function handleNewMessage(event) {

}

In addition to this, we'll need one more global variable called tabSenderId, this will be a unique ID we'll use to determine if a message was sent from the tab we're currently on. To start, let's add a utility method to the bottom of our script element to generate a unique ID. Let's also add our tabSenderId with our other global variables.

const tabSenderId = getUniqueId();

/***/

function getUniqueId() {
  return '_' + Math.random().toString(36).substr(2, 9);
}

Now, we're also going to want to persist the generated tabSenderId so that when we refresh our browser tabs this remains the same. We'll use SessionStorage for this, which isn't shared across tabs. Let's create a helper function called getSenderId that will attempt to find a SessionStorage item with the key of our senderIdKey. If an item is found we'll use that value as our tabSenderId, otherwise, we'll use our getUniqueId utility function to create a new unique ID that we'll then store in SessionStorage under the key of our senderIdKey.

const tabSenderId = getSenderId();

/***/

function getSenderId() {
  let senderId = sessionStorage.getItem(senderIdKey) || getUniqueId();
  sessionStorage.setItem(senderIdKey, senderId);
  return senderId;
}

function getUniqueId() {
  return '_' + Math.random().toString(36).substr(2, 9);
}

Adding A Few Utilities

Before we go any further, let's add a few utilities that will help keep things concise from here out.

function hideEl(el) {
  el.classList.add('hidden');
}

function showEl(el) {
  el.classList.remove('hidden');
}

function parseArray(item) {
  if (!item || !item.length) return [];
  return !Array.isArray(item) ? JSON.parse(item) : item;
}

function getArrayDifference(primary, secondary, key) {
  return key
    ? primary.filter((x) => !secondary.find((y) => x[key] === y[key]))
    : primary.filter((item) => !secondary.includes(item));
}

function setStringifiedStorageItem(key, value) {
  localStorage.setItem(key, value ? JSON.stringify(value) : value);
}

function getParsedStorageItem(storageKey) {
  let stringValue = localStorage.getItem(storageKey);
  return stringValue ? JSON.parse(stringValue) : stringValue;
}

hideEl and showEl are pretty straightforward, but let's run through the rest.

  • parseArray will gracefully attempt to parse a stringified array, if needed

  • getArrayDifference will return an array with items in the primary array that aren't in our secondary array.

  • setStringifiedStorageItem will store an item in LocalStorage, stringifying it beforehand.

  • getParsedStorageItem will get an item from LocalStorage, parsing it's contents beforehand.

Getting, Making, and Adding Message Helpers

Now that we have our storage and form listeners setup, we can move into adding some helpers to keep our message logic clear and concise.

Getting Messages

function getMessages() {
  const messages = getParsedStorageItem(messagesKey);
  return !Array.isArray(messages) ? [] : messages;
}

This will get and return an array of our messages, or an empty array if we have none.

Adding A Message

function addMessage(message) {
  let messages = getMessages();
  messages.push(message);
  return messages;
}

We'll call this when we go to add a message

Making A Message

function makeMessage(message) {
  return {
    id: getUniqueId(),
    senderId: tabSenderId,
    message,
  };
}

And this when we want to make a new message. We'll have a unique ID per message, our tab's sender ID, and the message.

function makeMessageEl({ message, id, senderId }) {
  const li = document.createElement('li');
  const baseClassName = 'px-3 py-1 rounded';

  li.id = id;
  li.textContent = message;
  li.className =
    senderId == tabSenderId
      ? `${baseClassName} bg-blue-600 text-white place-self-end`
      : `${baseClassName} bg-gray-700 text-white place-self-start`;

  return li;
}

We'll use this to create a message LI element to display our messages. If the message's senderId equals our tabSenderId, the message will be blue and aligned-right. Otherwise, it'll be gray and aligned-left.

Displaying Messages

function displayMessages(messages) {
  messages.forEach((msg) => messagesEl.appendChild(makeMessageEl(msg)));
}

We'll use this to append our created message LI's from makeMessageEl to the DOM under our messages list.

Clearing All Messages

Good news, we're done with helpers and setup! Let's now add our logic for clearing all our messages.

so, if you recall, earlier we created a handleClearMessages function that took an event. Let's adjust that method to the below.

function handleClearMessages(event) {
  if (event) event.preventDefault();

  setStringifiedStorageItem(messagesKey, []);
  messagesEl.innerHTML = '';
  showEl(messagesPlaceholderEl);
}

To make this function callable from our other methods, we'll check to see if we were given an event. If we were all we want to do is preventDefault.

Then we'll set our message LocalStorage value to an empty array, empty our message OL element's inner HTML, and show our "No messages" placeholder.

Handling New Messages

Now we're ready for our logic to handle new messages sent from other tabs. Let's edit the handleNewMessage method we added earlier to the below.

function handleNewMessage({ key, newValue, oldValue = getMessages() }) {
  newValue = parseArray(newValue);
  oldValue = parseArray(oldValue);

  if (!newValue || !newValue.length) {
    return handleClearMessages();
  }

  const newMessages = getArrayDifference(newValue, oldValue, 'id');
  displayMessages(newMessages);
  hideEl(messagesPlaceholderEl);
}

Here we're spreading our event object into { key, newValue, oldValue }, plucking just the properties we care about so we can directly reference them. Additionally, for the oldValue, we're defaulting the value if one isn't provided to our messages array. This allows us to call the method ourselves without needing to have the old array one hand.

We're also ensuring our new and old values are arrays by calling our utility parseArray function.

Next, if we don't receive a newValue, then we can assume we're clearing our values, so we'll call our handleClearMessages function.

If we do have a newValue then we want to grab only the new messages from our newValue by diffing the new and old values. We'll then add those new messages to the DOM by calling displayMessages and we'll ensure our "No messages" placeholder is hidden.

I hate going this long without being able to check our work, but trust me we we're getting there.

Sending A New Message

Finally, we're ready to add the ability to send a new message. Let's adjust the handleSendMessage function we added earlier to the below.

function handleSendMessage(event) {
  event.preventDefault();

  const value = event.target.message.value;
  const message = makeMessage(value);
  const messages = addMessage(message);

  handleNewMessage({ key: messagesKey, newValue: messages });
  setStringifiedStorageItem(messagesKey, messages);

  event.target.reset();
  event.target.message.focus();
}

We grab the value from the message form input off, make a new message matching our { id, senderId, message } object format. We'll add that new message to our existing messages and get the concatenation back as messages.

Next, we'll call our handleNewMessage function with our new message. Remember our storage event listener won't be called for the tab that's making the storage change, which is why we're manually calling this ourselves.

Then we'll update our LocalStorage message value.

Finally, we'll reset our form and refocus the message input.

Let's Test!

We can now finally test what we have! So open up two browser tabs pointing to the /localstorage page. If you've had these pages open previously, give them both a refresh. Now, send a message and you should see that message gets added both for the tab you're on and the inactive tab. Huzzah!

Now refresh the page and you'll see why we aren't quite done. Our messages are saved in LocalStorage, but we aren't doing anything to actually populate them on load.

Populating Messages On Load

To do this, let's add one more function to our page called loadMessages.

function loadMessages() {
  const messages = getMessages();

  if (messages.length) {
    hideEl(messagesPlaceholderEl);
  }

  displayMessages(messages);
}

This gets our messages from LocalStorage, determines whether to hide our "No messages" placeholder, and displays any messages we had stored.

Finally, let's call our loadMessages function.

loadMessages();

Now refresh the page again and you should see your messages populating on load.

Summary

While we did a lot in this lesson, the actual code needed to communicate across tabs was actually pretty minimal. All we needed to do was add an event listener to the window for the storage event and handle the change accordingly.

In the next lesson, we'll be covering how to do the exact same thing using the SharedWorker API.

Comment

Prepared By

Tom Gobich

Burlington, KY

Owner of Adocasts, JavaScript developer, educator, PlayStation gamer, burrito eater.

Visit Website