Skip to content

Cross-Tab Shared State

Imagine a multi-tab dashboard. Tab A increments a counter. Tab B should see the new value without polling. The storage event on localStorage can notify Tab B, but it only carries the raw new string value — there is no structured state layer built in.

A robust solution combines three primitives:

  1. localStorage (or IndexedDB) — the persistent source of truth.
  2. BroadcastChannel — instant notification to all other tabs when state changes.
  3. Web Locks — prevent two tabs from writing at the same time (leader election or write serialisation).

The simplest pattern: every tab that writes state also broadcasts the change.

const ch = new BroadcastChannel('demo:app-state');
async function setState(key, value) {
await navigator.locks.request('demo:state-write', async () => {
localStorage.setItem(key, JSON.stringify(value));
ch.postMessage({ type: 'STATE_UPDATE', key, value });
});
}
// React to changes from other tabs
ch.onmessage = (e) => {
if (e.data.type === 'STATE_UPDATE') {
console.log('Remote update:', e.data.key, '=', e.data.value);
// Update local UI here
}
};

The lock ensures that if two tabs try to write simultaneously, they queue rather than race.

For expensive work (e.g., a single tab polling an API), you want exactly one “leader” tab to do the work and broadcast results to followers. Web Locks make this straightforward:

async function runAsLeader() {
// Only one tab can hold 'leader' at a time.
// The lock is held for the entire async block — when this tab closes the lock
// releases and the next queued tab becomes leader.
await navigator.locks.request('demo:leader', async () => {
console.log('I am the leader');
while (true) {
const data = await fetchLatestData();
ch.postMessage({ type: 'DATA_REFRESH', data });
await new Promise((r) => setTimeout(r, 5000));
}
});
}
runAsLeader();

When the leader tab is closed, the browser releases its lock and the next queued tab’s callback fires — automatic leader handoff with no extra code.

Browser Storage
Why wrap a localStorage write in navigator.locks.request when multiple tabs may write?
In the leader-election pattern, what happens when the leader tab is closed?
Which storage API is used as the persistent source of truth in the broadcast-on-write pattern shown above?
Does the tab that calls BroadcastChannel.postMessage receive its own message event?