How would you keep multiple tabs in sync
Options: the `storage` event (fires in OTHER tabs when localStorage changes), the BroadcastChannel API (purpose-built tab-to-tab messaging), or a SharedWorker. Common uses: sync auth/logout, theme, cart. Watch ordering, the originating tab not receiving its own event, and serialization.
Browser tabs of the same origin are isolated, but several mechanisms let them communicate and stay in sync.
Option 1: the storage event
When localStorage (or sessionStorage) changes, a storage event fires **in all other same-origin tabs** — not the tab that made the change.
window.addEventListener("storage", (e) => {
if (e.key === "theme") applyTheme(e.newValue);
if (e.key === "auth" && !e.newValue) logout(); // another tab logged out
});- Pros — zero setup, widely supported, and the data is already persisted.
- Cons — only fires in other tabs (not the originator), only triggers on actual value changes, values are strings (serialize/parse), and you're coupling messaging to storage.
Option 2: BroadcastChannel API
A purpose-built pub/sub channel for same-origin contexts (tabs, iframes, workers):
const channel = new BroadcastChannel("app");
channel.postMessage({ type: "logout" });
channel.onmessage = (e) => { if (e.data.type === "logout") logout(); };- Pros — clean API, sends structured data (not just strings), decoupled from storage, no persistence side effect.
- Cons — ephemeral (no persistence — new tabs miss past messages), slightly less universal support (now broadly fine).
Option 3: SharedWorker
A single worker shared across all tabs — they connect to it and it relays/coordinates. Most powerful (shared state, a single WebSocket connection for all tabs) but most complex; reach for it when you genuinely need shared compute/connection, not just messaging.
Other approaches
- Service Worker — can broadcast to all controlled clients.
- Polling a shared store (last resort).
What you sync and the gotchas
Common: auth state / logout (log out everywhere at once), theme, cart, feature flags, "data updated, refetch."
Gotchas:
- Originating tab doesn't get its own
storageevent — update its own state directly. - Message ordering and races between tabs.
- Serialization —
storageis strings only. - A "leader tab" pattern if only one tab should do something (e.g. hold the WebSocket) — elect one via BroadcastChannel/locks.
- New tabs need initial state from storage/server, not just live messages.
The framing
"Three main tools. The storage event — fires in other tabs when localStorage changes; zero setup and the data's already persisted, but it's string-only and the originating tab doesn't get it. The BroadcastChannel API — purpose-built tab-to-tab pub/sub with structured messages, cleaner and decoupled from storage, though ephemeral. And a SharedWorker when you need genuine shared state or a single connection across tabs. Common uses are syncing logout, theme, and cart. The gotchas: the originator doesn't receive its own storage event, message ordering, and that new tabs need to hydrate initial state from storage or the server, not just live messages."
Follow-up questions
- •Why doesn't the originating tab receive its own storage event?
- •When would you use BroadcastChannel over the storage event?
- •When is a SharedWorker the right choice?
- •How do you elect a 'leader' tab?
Common mistakes
- •Expecting the storage event in the tab that made the change.
- •Forgetting storage values are strings (no auto-serialization).
- •Not hydrating new tabs' initial state — relying only on live messages.
- •Ignoring message ordering / race conditions between tabs.
Performance considerations
- •These mechanisms are lightweight, but a broadcast that triggers expensive work in every tab multiplies cost — debounce reactions, and consider a leader tab for expensive shared work like a single WebSocket connection.
Edge cases
- •A new tab opened after messages were already sent.
- •Many tabs all reacting to one event simultaneously.
- •Only one tab should act (leader election).
- •Private mode / storage disabled.
Real-world examples
- •Logging out of all tabs at once when one tab logs out.
- •Syncing theme or cart across tabs; a leader tab holding the app's WebSocket.