The Broadcast Channel API lets same-origin browsing contexts exchange messages through a named channel. Implement a React hook that owns one BroadcastChannel, delivers incoming data to the latest callback, and closes the native resource when its name changes or the component unmounts. The hook must also remain safe in server rendering and browsers without the API.
function useBroadcastChannel(
name: string,
onMessage: (data: unknown, event: MessageEvent) => void
): {
isSupported: boolean;
postMessage: (data: unknown) => void;
}
const { result } = renderHook(() =>
useBroadcastChannel('account', (data) => console.log(data))
);
// After the effect opens the channel:
result.current.isSupported; // true
result.current.postMessage({ type: 'signed-out' }); // undefined
// An incoming MessageEvent contains { type: 'theme', value: 'dark' }.
// Your callback receives both values, in this order:
onMessage({ type: 'theme', value: 'dark' }, originalMessageEvent);
isSupported: false during the first render; only an effect may construct the browser API.onMessage identity must take effect without reopening the channel.postMessage function referentially stable. It forwards the exact value, returns the native result, and preserves native errors.name removes the exact old listener and closes the old channel before opening the new one; unmount performs the same cleanup.postMessage throw a clear error.messageerror, use localStorage, or define an application message protocol.The hook turns a browser-owned messaging resource into a predictable React lifecycle.
Imagine two tabs showing the same account. When one tab signs out, it can publish an event and the other tab can react immediately. A BroadcastChannel handles that cross-context delivery, while your hook must handle React rerenders, cleanup, and unsupported environments.
Treat the channel as a subscription identified by name. This hook owns one endpoint; other same-origin endpoints with the same name receive its posts, but the sending endpoint does not receive its own message.
function useBroadcastChannel(name, onMessage) {
const channel = new window.BroadcastChannel(name);
channel.onmessage = (event) => onMessage(event.data, event);
return {
isSupported: true,
postMessage: (data) => channel.postMessage(data),
};
}
This creates a channel during rendering, so server rendering throws and every rerender leaks another native object. The returned sender also changes identity every render. Nothing removes the handler or calls close(), so old subscriptions can keep receiving messages after the component is gone.
const { useState, useRef, useEffect, useCallback } = require('react');
function useBroadcastChannel(name, onMessage) {
const [isSupported, setIsSupported] = useState(false);
const channelRef = useRef(null);
const onMessageRef = useRef(onMessage);
// The listener stays attached while this ref always points at fresh logic.
onMessageRef.current = onMessage;
useEffect(() => {
setIsSupported(false);
channelRef.current = null;
if (typeof window.BroadcastChannel !== 'function') return;
let channel;
try {
channel = new window.BroadcastChannel(name);
} catch {
return;
}
const handleMessage = (event) => {
onMessageRef.current(event.data, event);
};
channelRef.current = channel;
channel.addEventListener('message', handleMessage);
setIsSupported(true);
return () => {
channel.removeEventListener('message', handleMessage);
channel.close();
// Do not let a retained sender reach a channel after cleanup.
if (channelRef.current === channel) channelRef.current = null;
};
}, [name]);
const postMessage = useCallback((data) => {
const channel = channelRef.current;
if (!channel) throw new Error('BroadcastChannel is not open');
return channel.postMessage(data);
}, []);
return { isSupported, postMessage };
}
module.exports = { useBroadcastChannel };
The effect makes construction and teardown follow the channel name rather than every render. channelRef gives the stable sender access to the currently open object. A second ref separates callback freshness from subscription identity, so changing onMessage does not close and reopen the channel.
The constructor guard handles both missing browser support and construction failures. The hook intentionally does not catch errors from postMessage: clone failures and closed-channel errors belong to the caller, just as they do with the native API.
You render with the name account and callback A. The effect constructs one BroadcastChannel('account'), adds one listener, and marks the hook supported. A rerender supplies callback B; only onMessageRef.current changes, so the same native channel stays open. When another tab sends { type: 'signed-out' }, the listener reads the ref and calls callback B with that object and the original MessageEvent. If the name becomes presence, React runs the old effect's cleanup, removes its exact listener, closes the account channel, and then opens presence.
onMessage — callback identity changes can reopen the channel on every render; keep the latest callback in a ref.close() — removing a listener alone leaves the native endpoint connected; remove the exact listener and close its channel.postMessage already uses the structured clone algorithm; pass the original value to the browser.BroadcastChannel object does not receive its own post; use a second endpoint when testing real delivery.messageerror event when your product needs diagnostics for deserialization failures.The API is limited to compatible browsing contexts in the same origin and storage partition. Values are copied with the structured clone algorithm, and calling close() is required to release an endpoint the browser can no longer infer you need.
Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
The Broadcast Channel API lets same-origin browsing contexts exchange messages through a named channel. Implement a React hook that owns one BroadcastChannel, delivers incoming data to the latest callback, and closes the native resource when its name changes or the component unmounts. The hook must also remain safe in server rendering and browsers without the API.
function useBroadcastChannel(
name: string,
onMessage: (data: unknown, event: MessageEvent) => void
): {
isSupported: boolean;
postMessage: (data: unknown) => void;
}
const { result } = renderHook(() =>
useBroadcastChannel('account', (data) => console.log(data))
);
// After the effect opens the channel:
result.current.isSupported; // true
result.current.postMessage({ type: 'signed-out' }); // undefined
// An incoming MessageEvent contains { type: 'theme', value: 'dark' }.
// Your callback receives both values, in this order:
onMessage({ type: 'theme', value: 'dark' }, originalMessageEvent);
isSupported: false during the first render; only an effect may construct the browser API.onMessage identity must take effect without reopening the channel.postMessage function referentially stable. It forwards the exact value, returns the native result, and preserves native errors.name removes the exact old listener and closes the old channel before opening the new one; unmount performs the same cleanup.postMessage throw a clear error.messageerror, use localStorage, or define an application message protocol.The hook turns a browser-owned messaging resource into a predictable React lifecycle.
Imagine two tabs showing the same account. When one tab signs out, it can publish an event and the other tab can react immediately. A BroadcastChannel handles that cross-context delivery, while your hook must handle React rerenders, cleanup, and unsupported environments.
Treat the channel as a subscription identified by name. This hook owns one endpoint; other same-origin endpoints with the same name receive its posts, but the sending endpoint does not receive its own message.
function useBroadcastChannel(name, onMessage) {
const channel = new window.BroadcastChannel(name);
channel.onmessage = (event) => onMessage(event.data, event);
return {
isSupported: true,
postMessage: (data) => channel.postMessage(data),
};
}
This creates a channel during rendering, so server rendering throws and every rerender leaks another native object. The returned sender also changes identity every render. Nothing removes the handler or calls close(), so old subscriptions can keep receiving messages after the component is gone.
const { useState, useRef, useEffect, useCallback } = require('react');
function useBroadcastChannel(name, onMessage) {
const [isSupported, setIsSupported] = useState(false);
const channelRef = useRef(null);
const onMessageRef = useRef(onMessage);
// The listener stays attached while this ref always points at fresh logic.
onMessageRef.current = onMessage;
useEffect(() => {
setIsSupported(false);
channelRef.current = null;
if (typeof window.BroadcastChannel !== 'function') return;
let channel;
try {
channel = new window.BroadcastChannel(name);
} catch {
return;
}
const handleMessage = (event) => {
onMessageRef.current(event.data, event);
};
channelRef.current = channel;
channel.addEventListener('message', handleMessage);
setIsSupported(true);
return () => {
channel.removeEventListener('message', handleMessage);
channel.close();
// Do not let a retained sender reach a channel after cleanup.
if (channelRef.current === channel) channelRef.current = null;
};
}, [name]);
const postMessage = useCallback((data) => {
const channel = channelRef.current;
if (!channel) throw new Error('BroadcastChannel is not open');
return channel.postMessage(data);
}, []);
return { isSupported, postMessage };
}
module.exports = { useBroadcastChannel };
The effect makes construction and teardown follow the channel name rather than every render. channelRef gives the stable sender access to the currently open object. A second ref separates callback freshness from subscription identity, so changing onMessage does not close and reopen the channel.
The constructor guard handles both missing browser support and construction failures. The hook intentionally does not catch errors from postMessage: clone failures and closed-channel errors belong to the caller, just as they do with the native API.
You render with the name account and callback A. The effect constructs one BroadcastChannel('account'), adds one listener, and marks the hook supported. A rerender supplies callback B; only onMessageRef.current changes, so the same native channel stays open. When another tab sends { type: 'signed-out' }, the listener reads the ref and calls callback B with that object and the original MessageEvent. If the name becomes presence, React runs the old effect's cleanup, removes its exact listener, closes the account channel, and then opens presence.
onMessage — callback identity changes can reopen the channel on every render; keep the latest callback in a ref.close() — removing a listener alone leaves the native endpoint connected; remove the exact listener and close its channel.postMessage already uses the structured clone algorithm; pass the original value to the browser.BroadcastChannel object does not receive its own post; use a second endpoint when testing real delivery.messageerror event when your product needs diagnostics for deserialization failures.The API is limited to compatible browsing contexts in the same origin and storage partition. Values are copied with the structured clone algorithm, and calling close() is required to release an endpoint the browser can no longer infer you need.
Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.