Rooks
HooksBrowser APIs

useBroadcastChannel

Enables cross-tab/window communication via the BroadcastChannel API.

About

A React hook that provides a clean interface to the Broadcast Channel API for cross-tab/window communication. This hook allows you to send and receive messages between different browser tabs, windows, or workers of the same origin, enabling real-time synchronization across multiple instances of your application.

Examples

import { useState } from "react";
import { useBroadcastChannel } from "rooks";

export default function CrossTabStatus() {
  const [lastMessage, setLastMessage] = useState("No messages yet");
  const { isSupported, postMessage } = useBroadcastChannel<string>(
    "rooks-docs",
    { onMessage: setLastMessage }
  );

  return (
    <section>
      <p>{isSupported ? lastMessage : "BroadcastChannel is unavailable"}</p>
      <button
        type="button"
        onClick={() => postMessage("Hello from another tab")}
      >
        Broadcast message
      </button>
    </section>
  );
}

Basic cross-tab communication

import { useBroadcastChannel } from "rooks";
import { useState } from "react";

export default function CrossTabChat() {
  const [message, setMessage] = useState("");
  const [messages, setMessages] = useState([]);
  const [username, setUsername] = useState("User1");

  const { postMessage, isSupported } = useBroadcastChannel("chat-channel", {
    onMessage: (data) => {
      setMessages((prev) => [
        ...prev,
        {
          ...data,
          timestamp: new Date().toLocaleTimeString(),
        },
      ]);
    },
    onError: (error) => {
      console.error("Broadcast channel error:", error);
    },
  });

  const sendMessage = () => {
    if (message.trim()) {
      const messageData = {
        username,
        message: message.trim(),
        id: Date.now(),
      };
      postMessage(messageData);
      setMessage("");
    }
  };

  const handleKeyPress = (e) => {
    if (e.key === "Enter") {
      sendMessage();
    }
  };

  if (!isSupported) {
    return (
      <div style={{ padding: "20px" }}>
        <h3>Cross-Tab Chat</h3>
        <p style={{ color: "red" }}>
          BroadcastChannel API is not supported in this browser.
        </p>
      </div>
    );
  }

  return (
    <div style={{ padding: "20px", maxWidth: "500px" }}>
      <h3>Cross-Tab Chat</h3>
      <p>Open this page in multiple tabs to test cross-tab communication!</p>

      <div style={{ marginBottom: "20px" }}>
        <input
          type="text"
          value={username}
          onChange={(e) => setUsername(e.target.value)}
          placeholder="Your username"
          style={{ padding: "5px", marginRight: "10px", width: "120px" }}
        />
      </div>

      <div
        style={{
          height: "200px",
          overflowY: "auto",
          border: "1px solid #ccc",
          padding: "10px",
          marginBottom: "10px",
          backgroundColor: "#f9f9f9",
        }}
      >
        {messages.length === 0 ? (
          <p style={{ color: "#666", textAlign: "center" }}>
            No messages yet. Start typing!
          </p>
        ) : (
          messages.map((msg) => (
            <div key={msg.id} style={{ marginBottom: "8px" }}>
              <strong>{msg.username}</strong>
              <span style={{ color: "#666", fontSize: "0.8em" }}>
                {" "}
                ({msg.timestamp})
              </span>
              <div>{msg.message}</div>
            </div>
          ))
        )}
      </div>

      <div style={{ display: "flex", gap: "10px" }}>
        <input
          type="text"
          value={message}
          onChange={(e) => setMessage(e.target.value)}
          onKeyPress={handleKeyPress}
          placeholder="Type a message..."
          style={{ flex: 1, padding: "5px" }}
        />
        <button onClick={sendMessage} disabled={!message.trim()}>
          Send
        </button>
      </div>
    </div>
  );
}

Real-time counter synchronization

import { useBroadcastChannel } from "rooks";
import { useState, useEffect } from "react";

export default function SyncedCounter() {
  const [count, setCount] = useState(0);
  const [tabId] = useState(() => `tab-${Date.now()}-${Math.random()}`);

  const { postMessage, isSupported } = useBroadcastChannel("counter-sync", {
    onMessage: (data) => {
      if (data.type === "COUNTER_UPDATE" && data.tabId !== tabId) {
        setCount(data.count);
      }
    },
  });

  const updateCounter = (newCount) => {
    setCount(newCount);
    postMessage({
      type: "COUNTER_UPDATE",
      count: newCount,
      tabId,
    });
  };

  const increment = () => updateCounter(count + 1);
  const decrement = () => updateCounter(count - 1);
  const reset = () => updateCounter(0);

  if (!isSupported) {
    return (
      <div style={{ padding: "20px" }}>
        <h3>Synced Counter</h3>
        <p style={{ color: "red" }}>BroadcastChannel API not supported</p>
      </div>
    );
  }

  return (
    <div style={{ padding: "20px", textAlign: "center" }}>
      <h3>Synced Counter</h3>
      <p>Open in multiple tabs - the counter stays synchronized!</p>

      <div style={{ fontSize: "48px", margin: "20px 0", color: "#2196F3" }}>
        {count}
      </div>

      <div style={{ display: "flex", gap: "10px", justifyContent: "center" }}>
        <button onClick={decrement} style={{ padding: "10px 20px" }}>
          - Decrement
        </button>
        <button onClick={reset} style={{ padding: "10px 20px" }}>
          Reset
        </button>
        <button onClick={increment} style={{ padding: "10px 20px" }}>
          + Increment
        </button>
      </div>

      <p style={{ color: "#666", fontSize: "0.9em", marginTop: "20px" }}>
        Tab ID: {tabId}
      </p>
    </div>
  );
}

Shopping cart synchronization

import { useBroadcastChannel } from "rooks";
import { useState } from "react";

export default function SyncedShoppingCart() {
  const [cart, setCart] = useState([]);
  const [availableItems] = useState([
    { id: 1, name: "Laptop", price: 999 },
    { id: 2, name: "Mouse", price: 25 },
    { id: 3, name: "Keyboard", price: 75 },
    { id: 4, name: "Monitor", price: 299 },
  ]);

  const { postMessage, isSupported } = useBroadcastChannel("cart-sync", {
    onMessage: (data) => {
      if (data.type === "CART_UPDATE") {
        setCart(data.cart);
      }
    },
  });

  const updateCart = (newCart) => {
    setCart(newCart);
    postMessage({
      type: "CART_UPDATE",
      cart: newCart,
    });
  };

  const addToCart = (item) => {
    const existingItem = cart.find((cartItem) => cartItem.id === item.id);
    if (existingItem) {
      updateCart(
        cart.map((cartItem) =>
          cartItem.id === item.id
            ? { ...cartItem, quantity: cartItem.quantity + 1 }
            : cartItem
        )
      );
    } else {
      updateCart([...cart, { ...item, quantity: 1 }]);
    }
  };

  const removeFromCart = (itemId) => {
    updateCart(cart.filter((item) => item.id !== itemId));
  };

  const updateQuantity = (itemId, quantity) => {
    if (quantity <= 0) {
      removeFromCart(itemId);
    } else {
      updateCart(
        cart.map((item) => (item.id === itemId ? { ...item, quantity } : item))
      );
    }
  };

  const clearCart = () => updateCart([]);

  const total = cart.reduce((sum, item) => sum + item.price * item.quantity, 0);

  if (!isSupported) {
    return (
      <div style={{ padding: "20px" }}>
        <h3>Synced Shopping Cart</h3>
        <p style={{ color: "red" }}>BroadcastChannel API not supported</p>
      </div>
    );
  }

  return (
    <div style={{ padding: "20px", maxWidth: "800px" }}>
      <h3>Synced Shopping Cart</h3>
      <p>Add items to cart and see them sync across tabs!</p>

      <div style={{ display: "flex", gap: "20px" }}>
        {/* Available Items */}
        <div style={{ flex: 1 }}>
          <h4>Available Items</h4>
          {availableItems.map((item) => (
            <div
              key={item.id}
              style={{
                display: "flex",
                justifyContent: "space-between",
                alignItems: "center",
                padding: "10px",
                border: "1px solid #ddd",
                marginBottom: "5px",
              }}
            >
              <div>
                <div>{item.name}</div>
                <div style={{ color: "#666" }}>${item.price}</div>
              </div>
              <button onClick={() => addToCart(item)}>Add to Cart</button>
            </div>
          ))}
        </div>

        {/* Shopping Cart */}
        <div style={{ flex: 1 }}>
          <div
            style={{
              display: "flex",
              justifyContent: "space-between",
              alignItems: "center",
            }}
          >
            <h4>Shopping Cart ({cart.length} items)</h4>
            <button onClick={clearCart} disabled={cart.length === 0}>
              Clear Cart
            </button>
          </div>

          {cart.length === 0 ? (
            <p style={{ color: "#666" }}>Cart is empty</p>
          ) : (
            <>
              {cart.map((item) => (
                <div
                  key={item.id}
                  style={{
                    display: "flex",
                    justifyContent: "space-between",
                    alignItems: "center",
                    padding: "10px",
                    border: "1px solid #ddd",
                    marginBottom: "5px",
                  }}
                >
                  <div>
                    <div>{item.name}</div>
                    <div style={{ color: "#666" }}>
                      ${item.price} × {item.quantity} = $
                      {item.price * item.quantity}
                    </div>
                  </div>
                  <div
                    style={{
                      display: "flex",
                      gap: "5px",
                      alignItems: "center",
                    }}
                  >
                    <button
                      onClick={() => updateQuantity(item.id, item.quantity - 1)}
                    >
                      -
                    </button>
                    <span>{item.quantity}</span>
                    <button
                      onClick={() => updateQuantity(item.id, item.quantity + 1)}
                    >
                      +
                    </button>
                    <button
                      onClick={() => removeFromCart(item.id)}
                      style={{ marginLeft: "10px", color: "red" }}
                    >
                      Remove
                    </button>
                  </div>
                </div>
              ))}

              <div
                style={{
                  padding: "15px",
                  background: "#f5f5f5",
                  fontWeight: "bold",
                  fontSize: "1.2em",
                }}
              >
                Total: ${total}
              </div>
            </>
          )}
        </div>
      </div>
    </div>
  );
}

Parameters

ArgumentTypeDescriptionDefault
channelNamestringThe name of the broadcast channel to connect torequired
options{ onMessage?: (data: T) => void; onError?: (error: Event | Error) => void }Message and error callbacks{}

Options

OptionTypeDescriptionDefault
onMessage(data: T) => voidCallback function called when a message is receivedundefined
onError(error: Event | Error) => voidCalled for message deserialization, construction, or send errorsundefined

Return value

Returns an object with the following properties:

Return valueTypeDescriptionDefault
postMessage(data: T) => voidFunction to send a message to the broadcast channel-
close() => voidFunction to manually close the broadcast channel-
isSupportedbooleanWhether the BroadcastChannel API is supportedfalse

Browser Support

  • Chrome: 54+
  • Firefox: 38+
  • Safari: 15.4+
  • Edge: 79+

The hook includes built-in support detection and will gracefully handle unsupported browsers by setting isSupported to false.

Performance Notes

  • Messages are automatically serialized/deserialized using the structured clone algorithm
  • The hook automatically cleans up event listeners when the component unmounts
  • Other open channel objects with the same name receive the messages; the sending object does not receive its own post
  • Consider using unique channel names for different features to avoid message conflicts

Behavior and lifecycle

The effect creates one channel, listens for message and messageerror, and closes it when the name or callbacks change or the component unmounts. postMessage uses the structured-clone algorithm. Calling close closes the current channel immediately; it is not reopened until an effect dependency changes or the hook remounts. A sender does not receive its own message through the same channel object.

Compatibility and accessibility

SSR and unsupported browsers set isSupported: false; send/close calls then do nothing, with send warnings in development consoles. Creation and structured-clone failures are reported to onError when they are Event or Error objects. Validate received data as untrusted same-origin-context input and provide an in-page status when synchronization matters.

On this page