Using the BroadcastChannel API to Improve Communication Between Tabs in Angular

Milan Pevec
4 min readFeb 10, 2023

--

Usage of the BroadcastChannel API in our example by using an acknowledgement message.

Let’s begin by considering a scenario in which a user is working on a single page application (SPA). After completing some work, the user clicks on a button, which opens a new tab. In order for the original tab’s SPA to transfer a payload to the newly opened tab and its SPA, there are a few options that can be utilized.

One option is to use the window.postMessage, another is to use local storage object, but finally, you can also use the BroadcastChannel API, which allows you to create a communication channel between different tabs and windows. This API is supported in most modern browsers and offers a simple yet powerful way to exchange messages and data between tabs.

Checking the official description, we can deduct that API is pretty simple:

What’s the fuss then?

You might think this is a straightforward thing, but what if I tell you that:

the original tab (let’s call it TabA) needs to wait for the newly opened tab (let’s call it TabB) to be opened and for an Angular component to be instantiated there?

In other words, we need to delay communication from TabA until we receive an acknowledgement from TabB, something that will tell us that TabB is ready to receive the payload.

But let’s start with the big picture.

Back to the drawing board

As we can see in the picture above, conceptually our solution can be elaborated in the following steps:

  1. The Angular component on TabA opens a new TabB and subscribes to its acknowledgement message. Both parties have known IDs to establish ownership of the communication (otherwise, with many tabs we could run into a mess).
  2. The Angular component on TabB, when it is instantiated, sends an acknowledgement message to the original TabA and subscribes to receive a payload.
  3. Once TabA receives the acknowledgement from TabB, it sends the payload.

The payload can be basically anything, even a blob (check the specifications here).

There is a limitation, however: we must stay on the same domain (also schema), but that’s already our case.

All of this information needs to be compressed as much as possible, you can get the source code on my Github.

Bringing the theory to life: a look at the source code

Injection

First let’s create Injection Token for injecting service that will take care of all the details:

export const ACKNOWLEDGED_BROADCAST_CHANNEL = new InjectionToken('', {
providedIn: 'root',
factory() {
const zone = inject(NgZone);
const service = new AcknowledgedBroadcastChannelService('mpblog-acknowledged-broadcast-channel', zone);
return service;
},
});

export class AcknowledgedBroadcastChannelService {

}

As you can see, service is provided in root ie. it will be a singleton through the app. We are also going to use NgZone to wrap messaging into it to keep change detections in order. Last, we create and return an instance of a class, sending zone and name of the broadcast channel.

Now we can inject service in the component on TabA and TabB:

readonly channel = inject(ACKNOWLEDGED_BROADCAST_CHANNEL);

It's a good start. Let's continue.

Show me some types

We have four types defined within our implementation:

  • MessageType.

Represents the different types of messages that can be sent

  • EndpointId.

Identifies which endpoint sent an acknowledgement message for a specific MessageType

  • Message.

Is the content that is sent using the BroadcastChannel API.

  • Acknowledgements

Is a map that keeps a record of MessageTypes that have been acknowledged by one or more endpoints.

export enum MessageType {
USER_SELECTED_PROJECT_ID = 'user-selected-project-pid',
}

export type EndpointId = string;

export type Message = {
type: MessageType,
endpointId: EndpointId,
payload: JSONValue | null,
}

export type Acknowledgements = Record<MessageType, EndpointId[]> | Record<string, never>;

Implementation of a service

Let's just expose some of it. In the core, we have three private variables that everything revolves around:

private broadcastChannel: BroadcastChannel;
private broadcastChannel$: Subject<Message> = new Subject<Message>();
private acknowledgements$: BehaviorSubject<Acknowledgements> = new BehaviorSubject<Acknowledgements>({});

For clients the service exposes the following API:

acknowledge(type: MessageType, endpointId: EndpointId): void

broadcast(type: MessageType, endpointId: EndpointId, payload: JSONValue): void

onAcknowledge(type: MessageType, endpointId: EndpointId): Observable<boolean>

onBroadcast(type: MessageType): Observable<Message>

Internally, acknowledging something means simply sending a message with specific content. As you can see in the code below, the message listener checks the content. In the case of an acknowledgement, we handle it differently than for non-acknowledgement messages:

this.broadcastChannel.onmessage = (({ data }: { data: Message }) => {
if (data.payload === this.ackMessagePayload) {

} else {

}
});

Usage (TabA)

To follow our original plan, we should only send the project ID to another tab once an acknowledgement is received:

const endpointId: EndpointId = 'Project-Selector';
const newTabEndpointId: EndpointId = uuidv4();
const payload = {
pid,
};

this.channel.onAcknowledge(this.messageType, newTabEndpointId).pipe(
take(1), // we unsubscribe right away
).subscribe(() => {
this.channel.broadcast(this.messageType, endpointId, payload);
});

this.window.open(`tabcomm/project?eid=${newTabEndpointId}`);

Usage (TabB)

When a TabB component is initialized, we send the ack message to the original TabA and also subscribe to get the project id:

this.pid$ = this.channel.onBroadcast(this.messageType).pipe(
timeout(3500),
catchError(() => {
return of(null);
}),
map((message: Message | null) => message ? (message.payload as JSONObject)['pid'] as string : null),
take(1),
);

this.channel.acknowledge(this.messageType, this.endpointIdToBeAck);

What’s important here is to implement a fail-safe mechanism. In case the component does not receive the project ID, it should time out and set the value to null.

That's it, I hope it will be helpful to someone. Once again all the details can be found here.

--

--