Sync Shared State
messages is an event log of what other tabs sent, not the current state. To keep a piece of state (dashboard filters, user settings) in sync, update local state and send one message from the handler, then apply incoming messages with onMessage:
function Dashboard() { const [filters, setFilters] = useState({ status: 'all' });
const { postMessage } = useBroadcastChannel('dashboard', { // sourceName is optional: a unique one is generated for each tab. onMessage: { 'filters-update': msg => setFilters(msg.message), }, });
const updateFilters = (next: typeof filters) => { setFilters(next); postMessage('filters-update', next); };
return <FilterBar value={filters} onChange={updateFilters} />;}Do not rebroadcast from effects
Section titled “Do not rebroadcast from effects”Do not call postMessage from a useEffect on state that you also set from incoming messages. Each tab re-broadcasts what it receives, the tabs ping-pong, and they can end up with different values. Deduplication is by message ID, so it does not stop this. Send from the event handler, not from an effect.
New tabs do not receive past messages
Section titled “New tabs do not receive past messages”Messages are not replayed. A tab opened later only sees messages sent after it opened, and neither keepLatestMessage nor getLatestMessage changes that. Give the new tab a way to ask for the current state:
const { postMessage } = useBroadcastChannel('dashboard', { sourceName, // unique per tab onMessage: { 'filters-update': msg => setFilters(msg.message), 'state-request': () => postMessage('filters-update', filtersRef.current), },});
// Ask once when this tab mountsuseEffect(() => { postMessage('state-request', null);}, []);Keep the current value in a ref (filtersRef.current = filters) so the state-request handler always answers with the latest state. If no other tab is open, nothing answers and the tab keeps its default state. A fuller walkthrough is in Late-joining tabs.
