How to use the BroadcastChannel API with Angular blog post hero image
SOFTWARE DEVELOPMENTCUSTOM SOFTWAREANGULAR
18/06/2020 • Davy Steegen

How to use the BroadcastChannel API with Angular

Have you ever heard of the BroadcastChannel API? We hadn’t either just a couple of weeks ago. We happened to stumble upon it after looking for a solutions that allowed us to communicate between different browser windows of the same origin. In this blog post, we’ll discuss the API itself and teach you how to use the BroadcastChannel API within an Angular application.

The BroadcastChannel API

Imagine you opened a web page in multiple tabs, and you want to communicate between these tabs to keep them up-to-date. How would you even start to do that? After some digging around the internet, we came across the BroadcastChannel API that is implemented directly in web browsers.

It turns out that this API has already been available since 2015. Mozilla Firefox 38 was the first browser to adopt the specification. Over the course of the next few years, other browsers followed Mozilla’s example.

Check out the demo below to see an example of what can be done by utilizing this technology.

Broadcast Channel demo

Albeit a very simple demo, it nevertheless shows the true power of the BroadcastChannel API. In this example, the counter is kept in sync between the two windows. It may not be your typical real world example, but you could use the API to:

  • log out a user of an application that is running in multiple browser tabs,
  • keep a shopping cart in sync in another browser tab,
  • and refresh data in other tabs.

The BroadcastChannel is basically an event bus where you have a producer and one or more consumers of the event:

BroadcastChannel message

How to set up a BroadcastChannel

Creating a BroadcastChannel

Creating a BroadcastChannel is very simple. It doesn’t require any libraries to be imported in your code. You just need to invoke the constructor with a String that contains the name of the channel to be created.

const broadcastChannel = new BroadcastChannel('demo-broadcast-channel');

Sending a message

Now that we’ve set up a channel, we can use it to post messages. Posting a message can be done by calling the postMessage on the BroadcastChannel that you created earlier.

this.counter++;
 
broadcastChannel.postMessage({
  type: 'counter',
  counter: this.counter
  });
}

The postMessage can take all kinds of objects as message. You can basically send anything that you want, as long as the consumer knows how to handle the receiving objects. However, it’s good practice to have a field on your messages that describes the type of message it is. This makes it easier to subscribe to messages of a specific type instead of having a BroadcastChannel per type of message.

Receiving a message

On the consumer side, you’ll need to create a BroadcastChannel with the same name as on the producer side. If the names do not match, you (obviously) won’t receive any messages. Next, you need to implement the onmessage callback.

const broadcastChannel = new BroadcastChannel('demo-broadcast-channel');
 
this.broadcastChannel.onmessage = (message) => {
  console.log('Received message', message);
}

The BroadcastChannel that posts a message won’t receive the message itself, even if it has a listener registered. However, if you create a separate BroadcastChannel instance for posting and consuming messages, the browser window that posted the message will receive the message. Most likely, that’s not something you want. To avoid this, it is best practice to create a singleton instance per BroadcastChannel.

Creating a reusable BroadcastService

You don’t want to reference the BroadcastChannel API everywhere in you code where you need to produce/consume messages. Instead, let’s create a reusable service that encapsulates the logic. That way, if you ever want to replace the BroadcastChannel with another API, you only have to update one service.

import {Observable, Subject} from 'rxjs';
import {filter} from 'rxjs/operators';
 
interface BroadcastMessage {
  type: string;
  payload: any;
}
 
export class BroadcastService {
  private broadcastChannel: BroadcastChannel;
  private onMessage = new Subject<any>();
 
  constructor(broadcastChannelName: string) {
    this.broadcastChannel = new BroadcastChannel(broadcastChannelName);
    this.broadcastChannel.onmessage = (message) => this.onMessage.next(message.data);
  }
 
  publish(message: BroadcastMessage): void {
    this.broadcastChannel.postMessage(message);
  }
 
  messagesOfType(type: string): Observable<BroadcastMessage> {
    return this.onMessage.pipe(
      filter(message => message.type === type)
    );
  }
}

In this particular service, we made good use of RxJS Observables. Pay close attention to the messagesOfType function: in this case, we used the standard RxJS filter operator to only return the messages that match the provided type. Nice and simple!

The service is almost ready for use in your Angular application. There is only one more challenge that you’ll have to face.

Running inside the Angular Zone

If you’ve been using Angular for some time, you’ll probably know about the Angular Zone. Code that runs inside the Angular Zone will automatically trigger the change detection. 

The service above doesn’t run Angular’s zone, since it uses an API that does not hook into Angular. If it receives a message and updates the internal state of a component, Angular is not immediately aware of this. That means that you don’t immediately see any changes reflected in the browser. Only after the next change detection is triggered, will the results be visible inside the browser.

To work around this issue, you can create a custom RxJS OperatorFunction. The sole purpose of the OperatorFunction is to make sure that every life cycle hook of an Observable is running inside the Angular’s Zone.

import { Observable, OperatorFunction } from 'rxjs';
import { NgZone } from '@angular/core';
 
/**
 * Custom OperatorFunction that makes sure that all lifecycle hooks of an Observable
 * are running in the NgZone.
 */
export function runInZone<T>(zone: NgZone): OperatorFunction<T, T> {
  return (source) => {
    return new Observable(observer => {
      const onNext = (value: T) => zone.run(() => observer.next(value));
      const onError = (e: any) => zone.run(() => observer.error(e));
      const onComplete = () => zone.run(() => observer.complete());
      return source.subscribe(onNext, onError, onComplete);
    });
  };
}

NgZone is an object provided by Angular that you can use to programmatically run code inside Angular’s zone. The only thing that remains is to use the above OperatorFunction in our BroadcastService.

...
import {runInZone} from './run-in-zone';
 
export class BroadcastService {
...
 
  constructor(broadcastChannelName: string, private ngZone: NgZone) {
    this.broadcastChannel = new BroadcastChannel(broadcastChannelName);
    this.broadcastChannel.onmessage = (message) => this.onMessage.next(message.data);
  }
...
  messagesOfType(type: string): Observable<BroadcastMessage> {
    return this.onMessage.pipe(
      // It is important that we are running in the NgZone. This will make sure that Angular component changes are immediately visible in the browser when they are updated after receiving messages.
      runInZone(this.ngZone),
      filter(message => message.type === type)
    );
  }
}

After updating the service, changes will be visible immediately upon receiving messages.

Injecting the service

You can use Angular’s InjectionToken to create a singleton instance of the service. Declare the InjectionToken:

export const DEMO_BROADCAST_SERVICE_TOKEN = new InjectionToken<BroadcastService>('demoBroadcastService', {
  factory: () => {
    return new BroadcastService('demo-broadcast-channel');
  },
});

Inject the service via the InjectionToken:

constructor(@Inject(DEMO_BROADCAST_SERVICE_TOKEN) private broadcastService: BroadcastService) {
}

Is the BroadcastChannel API supported everywhere?

You have to keep the following in mind when using the BroadcastService. It’ll only work when

  • all browser windows are running on the same host and port,
  • all browser windows are using the same scheme (it will not work if one app is opened with https and the other with http),
  • the browser windows aren’t opened in incognito mode,
  • and your browser windows are opened in the same browser (there is no cross-browser compatibility).

All modern browsers support the BroadcastChannel API, except for Safari and Internet Explorer 11 (and below). For a full list of compatible browsers, check out Caniuse.

If you need to implement a similar solution in non-supported browsers, you can use the browser’s LocalStorage instead.

Takeaway

In this blog post, we briefly described how to make use of the browsers BroadcastChannel API inside an Angular application. We also looked at a solution on how the API can be hooked into Angular’s Zone. You can find the full code of the demo on Stackblitz. Moreover, you can consult the BroadcastChannel API documentation on MDN Web Docs.