A few months ago I was working on an app where users manage playlists, the kind of thing where people naturally end up with three or four tabs open without thinking about it.
A user added a track to a playlist in one tab, switched to another tab with that same playlist open, and it just wasn't there. Refresh, and it shows up. No refresh, and that tab has no idea anything happened.
It wasn't really a bug. The code did exactly what I told it to. I'd just built the app assuming there was only ever one tab. The moment there were two, neither knew the other existed.
That sent me down a rabbit hole: how do tabs of the same app actually talk to each other? Turns out the playlist thing was just one symptom of a bigger pattern. A cart that doesn't update in another tab. A user logged out in one tab but not another. A background job running five times instead of once.
Here's what I found.
Tabs are more isolated than you'd think
Each one is its own separate JavaScript world. They share cookies and localStorage, but nothing pushes a change from one tab to another automatically. If you want two tabs of the same app to know about each other, you have to build that connection on purpose.
The tool that ended up solving this for me was something called BroadcastChannel. It's a small, quietly powerful browser API that lets any tab shout a message that every other open tab of the same app can hear. No server involved, no polling, nothing fancy on the backend.
const channel = new BroadcastChannel('playlist-updates');
// In the tab where the track was added
channel.postMessage({ type: 'track-added', playlistId, track });
// In every other open tab
channel.onmessage = (event) => {
if (event.data.type === 'track-added') {
addTrackToLocalState(event.data.playlistId, event.data.track);
}
};
That's genuinely most of it. The moment I added this, the second tab started picking up changes the instant they happened in the first one. No refresh, no waiting.
The other place this showed up: logging out
Once I understood the pattern, I noticed a second, more serious version of the same problem. Logging out. A user logs out in one tab, but the others don't know, and they just keep sitting there looking logged in.
The fix was the same idea, just for a different event.
// When the user logs out in this tab
authChannel.postMessage({ type: 'logout' });
// Every other tab
authChannel.onmessage = (event) => {
if (event.data.type === 'logout') {
clearLocalSession();
redirectToLogin();
}
};
Same channel concept, same "tell the others what just happened" idea.
What else I found along the way
Digging into this opened up a couple of other things I hadn't come across before, even though I'd been writing frontend code for a while.
One was the Web Locks API. It solves a slightly different problem: instead of telling every tab about something, it lets you make sure only one tab does something at a time. Say your app keeps a single live connection open for updates. You don't want six tabs each opening their own. Web Locks lets tabs "compete" for a lock, one of them wins and does the job, and if that tab closes, another one just picks it up automatically. No manual bookkeeping, no polling to check who's still alive.
The other was realizing BroadcastChannel isn't the only option, just the one that fit my case best. There's also the older storage event, which fires on other tabs when localStorage changes, useful if you want something that's more of a side effect than a deliberate message. And SharedWorker, which is a whole separate background script that every tab connects to, handy when you want one shared brain instead of duplicated logic per tab.
What stuck with me most, though, wasn't any specific API. It was realizing how much of the code I write quietly assumes it's the only tab in the room, when in practice, users never really work that way😂
Top comments (0)