Skip to main content

useLeader

Your app is open in five tabs. Each one opens a WebSocket, each one polls /notifications every ten seconds, each one races the others to refresh the expiring auth token. That's five sockets and five refreshes where you wanted one.

useLeader elects exactly one tab to do the work. The others stand by, and if the elected tab goes away, another one picks the job up.

import { useLeaderEffect } from 'use-everywhere';

function LiveFeed() {
useLeaderEffect(() => {
const socket = new WebSocket('wss://example.com/feed');
return () => socket.close();
});

return <Feed />;
}

That's the whole thing. useLeaderEffect runs the effect only in the tab holding the seat, and runs the cleanup when that tab loses it. Every tab still renders <Feed /> — they just read the state the leader writes, which is what useSharedState is for.

The three hooks

const { leaderId, isLeader } = useLeader(); // who holds the seat
const isLeader = useIsLeader(); // just the boolean
useLeaderEffect(() => {
/* … */
}); // run only if we hold it

leaderId is null while the seat is empty — briefly on startup, and again for about a lease after a leader crashes.

How the seat moves

The incumbent is sticky. Opening a new tab does not disturb it: the sitting leader answers the newcomer's hello immediately, so the newcomer adopts it rather than starting an election. There is no leaderless flash, and no churn.

what happenswhat you see
One tab, aloneIt leads after ~1 heartbeat (1s).
A second tab opensCrown does not move. The newcomer follows.
The leader is closedHanded over immediately — it resigns on the way out.
The leader crashesThe seat is empty for leaseMs (3s), then a survivor claims it.

The reason closing is instant and crashing is not: a tab that closes gets a pagehide event and says so. A tab that is killed says nothing, and silence is the only evidence the others have — so they must wait long enough to be sure.

Opting a tab out

Eligibility is a property of the tab, not of a component. Set it in one place:

// This tab watches, but will never be asked to do the work.
const { leaderId } = useLeader({ eligible: false });

A common pattern is to keep background tabs out of the running, since their timers are throttled:

useLeader({ eligible: !document.hidden });

Because it's a per-tab property, a component that just reads useLeader() without passing eligible will not re-enrol a tab that opted out.

Tuning

useLeader({
name: 'feed', // which bus to elect on
heartbeatMs: 1000, // how often the leader re-announces
leaseMs: 3000, // how long followers tolerate silence
});

Lower leaseMs for faster failover, at the cost of more false demotions when a tab is throttled. The default tolerates a backgrounded tab's clamped timers.

Leadership is advisory

This is not a distributed lock. There is a window of about one round trip during which two tabs can both believe they lead — after a network-less BroadcastChannel hiccup, or when two claims genuinely cross — and a backgrounded tab whose timers are throttled can lose a lease it deserved to keep.

That is fine for "don't open five sockets" and wrong for "don't charge the card twice." Use it for efficiency, not for safety. See Limitations.