GitHub
Open Github

UseBroadcast

Sync state across multiple browser tabs in real time. Built on the BroadcastChannel API — no server, no WebSockets required.

Installation

pnpm add @hookraft/usebroadcast

Usage

import { useBroadcast } from "@hookraft/usebroadcast"
const { state, broadcast, status, isSupported, listenerCount } = useBroadcast("my-channel", initialState, {
  onMessage: (data, tabId) => console.log("Received from tab:", tabId, data),
  onTabJoin: (tabId) => console.log("New tab joined:", tabId),
  onTabLeave: (tabId) => console.log("Tab left:", tabId),
})

How it works

Your browser can have many tabs open at the same time. By default those tabs are completely isolated — what happens in Tab 1 stays in Tab 1.

useBroadcast connects tabs together using the browser's built-in BroadcastChannel API. The moment you call broadcast() in one tab, every other tab that uses the same channel name receives the update instantly — no server, no WebSockets required.

Tab 1 (user clicks logout)
  │
  │  broadcast(null)
  │
  ├──────────────────→ Tab 2 (automatically redirects to /login)
  ├──────────────────→ Tab 3 (automatically redirects to /login)
  └──────────────────→ Tab 4 (automatically redirects to /login)

Full Examples

Example 1 — Logout across all tabs

User logs out in one tab — every other open tab logs out automatically.

import { useBroadcast } from "@hookraft/usebroadcast"

function useAuthSync({ onLogout }: { onLogout: () => void }) {
  const { broadcast } = useBroadcast<{ loggedIn: boolean }>(
    "auth",
    { loggedIn: true },
    {
      onMessage: (data) => {
        if (!data.loggedIn) {
          // another tab logged out — react here
          onLogout()
        }
      },
    }
  )

  const logoutEverywhere = () => {
    // tell every other tab to log out
    broadcast({ loggedIn: false })
    // handle local logout yourself
    onLogout()
  }

  return { logoutEverywhere }
}
function LogoutButton() {
  const { logoutEverywhere } = useAuthSync({
    onLogout: () => {
      localStorage.removeItem("token")
      window.location.href = "/login"
    },
  })

  return (
    <button onClick={logoutEverywhere}>
      Logout everywhere
    </button>
  )
}

Example 2 — Sync cart across tabs

User adds an item to the cart in Tab 1. The cart badge in Tab 2 updates instantly.

import { useBroadcast } from "@hookraft/usebroadcast"

type CartItem = {
  id: string
  name: string
  price: number
  quantity: number
}

function useCart() {
  const { state: cart, broadcast } = useBroadcast<CartItem[]>("cart", [])

  const addItem = (item: CartItem) => {
    const updated = [...cart, item]
    broadcast(updated) // updates this tab AND all others
  }

  const removeItem = (id: string) => {
    const updated = cart.filter((i) => i.id !== id)
    broadcast(updated)
  }

  const clearCart = () => broadcast([])

  return {
    cart,
    addItem,
    removeItem,
    clearCart,
    totalItems: cart.reduce((acc, i) => acc + i.quantity, 0),
  }
}
function CartBadge() {
  const { totalItems } = useCart()

  return (
    <div>
      Cart
      {totalItems > 0 && <span>{totalItems}</span>}
    </div>
  )
}

Example 3 — Sync notification badge

New message arrives — every open tab shows the updated unread count instantly.

import { useBroadcast } from "@hookraft/usebroadcast"

function useNotifications() {
  const { state: unreadCount, broadcast } = useBroadcast<number>(
    "notifications",
    0
  )

  const markAllRead = () => broadcast(0)
  const increment = () => broadcast(unreadCount + 1)

  return { unreadCount, markAllRead, increment }
}
function NotificationBell() {
  const { unreadCount, markAllRead } = useNotifications()

  return (
    <button onClick={markAllRead}>
      🔔 {unreadCount > 0 && <span>{unreadCount}</span>}
    </button>
  )
}

Example 4 — Track how many tabs are open

import { useBroadcast } from "@hookraft/usebroadcast"

function TabCounter() {
  const { listenerCount, tabId, isSupported } = useBroadcast(
    "presence",
    null,
    {
      onTabJoin: (id) => console.log(`Tab ${id} joined`),
      onTabLeave: (id) => console.log(`Tab ${id} left`),
    }
  )

  if (!isSupported) {
    return <p>Your browser does not support cross-tab sync.</p>
  }

  return (
    <div>
      <p>Your tab ID: {tabId}</p>
      <p>Other tabs open: {listenerCount}</p>
    </div>
  )
}

broadcast vs send

These two look similar but behave differently:

const { state, broadcast, send } = useBroadcast("channel", 0)

// broadcast — updates THIS tab AND sends to all other tabs
broadcast(42)
// state in this tab = 42
// all other tabs receive 42 and update

// send — only sends to other tabs, does NOT update this tab
send(42)
// state in this tab stays unchanged
// other tabs receive 42

Use broadcast when you want all tabs in sync. Use send when you want to notify other tabs without changing your own state — for example a one-time signal like "refresh" or "reload".

Browser Support

useBroadcast is SSR safe. It checks for window before touching any browser API so it will not crash in server-side environments. If BroadcastChannel is not supported, isSupported returns false and the hook does nothing — no errors thrown.

const { isSupported } = useBroadcast("channel", null)

if (!isSupported) {
  // show a fallback or skip the feature
}

Options

PropTypeDefault
onMessage
(data: T, tabId: string) => void
-
onTabJoin
(tabId: string) => void
-
onTabLeave
(tabId: string) => void
-
syncState
boolean
true
onConnect
() => void
-
onDisconnect
() => void
-

Returns

PropTypeDefault
state
T
-
broadcast(value)
(value: T) => void
-
send(value)
(value: T) => void
-
status
BroadcastStatus
-
isSupported
boolean
-
tabId
string
-
listenerCount
number
-
close()
() => void
-
reconnect()
() => void
-