Techcookies

Broadcast channel with react and typescript

ReactJS, TypeScript, JavaScript | Tue Sep 03 2024 | 4 min read

Let's assume we have a scenario where we want to logout our opened application in another tab/window in the same browser in a React application or we have similar scenarios mentioned below:

  • Detect user actions in other tabs
  • Know when a user logs into an account in another window/tab.
  • Instruct a worker to perform some background work
  • Know when a service is done performing some action.
  • When the user uploads a photo in one window, pass it around to other open pages.
typescript
// src/hooks/useBroadcastChannel.ts

import { useMemo, useEffect, useCallback, useRef, ReactNode } from "react";

export type MessageType = string | object | [] | ReactNode;

const channelInstances: { [key: string]: BroadcastChannel } = {};

const getSingletonChannelInstance = (name: string): BroadcastChannel => {
  if (!channelInstances[name]) {
    channelInstances[name] = new BroadcastChannel(name);
  }
  return channelInstances[name];
};

export function useBroadcastChannel(
  channelName: string,
  onMessageHandler: (message: MessageType) => void
) {
  const isSubscribed = useRef(false);
  const channel = useMemo(
    () => getSingletonChannelInstance(channelName),
    [channelName]
  );
  useEffect(() => {
    if (!isSubscribed.current) {
      channel.onmessage = (event) => onMessageHandler(event.data);
    }
    return () => {
      if (isSubscribed.current) {
        isSubscribed.current = true;
      }
    };
  }, []);

  const postMessage = useCallback(
    (message: MessageType) => {
      channel?.postMessage(message);
    },
    [channel]
  );
  return {
    postMessage,
  };
}
typescript
// /src/components/broadcast.tsx
'use client';
import { FormEvent, useState } from "react";
import { useBroadcastChannel, MessageType } from "../hooks/useBroadcastChannel";

const Broadcast = () =>{
    const [msg, setMsg] = useState<MessageType>('');
    const handler = (message: MessageType) =>{
        console.log('message ', message?.toString())
        setMsg(message);
    }

    const {postMessage} = useBroadcastChannel('broadcast-channel', handler)
    const changeHandler = (event: FormEvent<HTMLInputElement>) => {
        postMessage(event?.currentTarget?.value.toString());
      };
    return (
        <>
         <input type="text" onChange={changeHandler} />
         <div>{String(msg)}</div>
        </>
    )
}
export default Broadcast;

A channel won't broadcast to itself. So if you have an onmessage listener on the same page as a postMessage() to the same channel, that message event doesn't fire.

Let's assume, we have a requirement for the same where we want to broadcast to the same page, for example: if from profile page we have triggered logout and want this to reflect to the other tab and i have receiving event listener on the same page. We can achieve this using 2 BroadcastChannel, following steps can help achieve the same.

  1. We can create two instances of BroadcastChannel.
  2. One can act as a broadcaster for messages and trigger postMessage.
  3. Other can be used for receiving messages and add event listener for onmessage.
typescript
// src/hooks/useBroadcastChannelSamePage.ts

import { useMemo, useEffect, useCallback, useRef, ReactNode } from "react";

export type MessageType = string | object | [] | ReactNode;

type ChannelType = {
  sender: BroadcastChannel;
  receiver: BroadcastChannel;
};
const channelInstances: { [key: string]: BroadcastChannel } = {};

const getSingletonChannelInstance = (name: string): ChannelType => {
  if (
    !channelInstances[name + "_sender"] &&
    !channelInstances[name + "_receiver"]
  ) {
    channelInstances[name + "_sender"] = new BroadcastChannel(name);
    channelInstances[name + "_receiver"] = new BroadcastChannel(name);
  }
  return {
    sender: channelInstances[name + "_sender"],
    receiver: channelInstances[name + "_receiver"],
  };
};

export function useBroadcastChannelSamePage(
  channelName: string,
  onMessageHandler: (message: MessageType) => void
) {
  const isSubscribed = useRef(false);
  const channel = useMemo(
    () => getSingletonChannelInstance(channelName),
    [channelName]
  );
  useEffect(() => {
    if (!isSubscribed.current) {
      channel.receiver.onmessage = (event) => onMessageHandler(event.data);
    }
    return () => {
      if (isSubscribed.current) {
        isSubscribed.current = true;
      }
    };
  }, []);

  const postMessage = useCallback(
    (message: MessageType) => {
      channel?.sender?.postMessage(message);
    },
    [channel]
  );
  return {
    postMessage,
  };
}
typescript
// /src/components/broadcast-same-page.tsx

"use client";
import { FormEvent, useState } from "react";
import {
  useBroadcastChannelSamePage,
  MessageType}
  from "../hooks/useBroadcastChannelSamePage";

const BroadCastPageSamePage = () => {
  const [msg, setMsg] = useState<MessageType>('');
  const handler = (message: MessageType) => {
    setMsg(message);
  };
  const { postMessage } = useBroadcastChannelSamePage("broadcast-ch", handler);
  const changeHandler = (event: FormEvent<HTMLInputElement>) => {
    postMessage(event?.currentTarget?.value.toString());
  };
  
  return (
    <>
      <input type="text" onChange={changeHandler} />
      <div>{String(msg)}</div>
    </>
  );
};
export default BroadCastPageSamePage;
alt text