Skip to main content

The mental model

Everything in use-everywhere follows from two ideas. Once they click, the whole API stops being something you look up and starts being something you can predict.

Idea 1: a value that exists in more than one place

useState gives you a value that exists in one component. Lift it to context and it exists in one React tree. use-everywhere lifts it one more level: the value exists in every tab, window, and worker on your origin at once.

const [count, setCount] = useSharedState('count', 0);

There is no "server tab" and no "main copy". Each tab holds its own replica, every write is broadcast, and the replicas converge. You get to pretend it's one object — and the library's whole job is keeping that pretense honest:

  • Writes appear everywhere within a few milliseconds.
  • Two tabs writing at the same moment end up agreeing on one winner — never a split brain.
  • A tab opened later immediately sees the current value, not the initial one.

The corollary is worth internalizing early: the value lives exactly as long as some context holds it. Close the last tab and the state is gone — nothing is persisted anywhere. That's a feature (nothing to clean up, nothing stale on disk), but it means use-everywhere replaces postMessage plumbing, not your database. See what it deliberately doesn't do.

Idea 2: two worlds, two trust levels

Browsers draw a hard line at the origin (scheme://host:port), and the library embraces that line rather than papering over it. Everything you do lives in one of two worlds:

Same originCross origin
Who is thereYour own tabs, windows, workersA page you opened on another domain
TrustFull — it is all your codeNone — it is another security principal
TopologyMany-to-many busStrict 1:1, parent ↔ child
TransportBroadcastChannelwindow.opener / postMessage
API surfaceuseSharedState, useMessage, usePeersopenWindow / connectToOpener
Shape of dataShared state and broadcast eventsExplicit typed messages and one result

Shared state deliberately never crosses the origin line. A foreign page that could merge writes into your state tree would be a confused deputy; across origins you exchange explicit, validated messages instead — request/result shaped, like the payment flow. The design in one line: across trust boundaries, explicit beats magic.

What a "name" really is

Every same-origin primitive takes a name. Behind one name there is exactly one bus per tab: one BroadcastChannel, one client identity, one heartbeat.

Three things follow, and each one answers a common question:

  1. The name is the identity. Two components using store 'checkout' in the same tab share one store instance; two tabs using 'checkout' are two peers on the same wire. This is also why there's no Provider: a BroadcastChannel is already global to the origin, so a React context couldn't scope it any further — wrapping it would be ceremony without meaning.
  2. One client id per tab per name. State patches, events, and presence all carry the same clientId, so "which tab changed this?" and "which presence dot is that tab?" have the same answer.
  3. Everything shares the wire. State sync, events, and presence heartbeats are multiplexed over the one channel, and any message from a peer doubles as proof of life — so chatty tabs never pay extra heartbeat cost.

The three same-origin views

The bus itself is internal. You consume it through three views, each answering a different question:

  • Shared state (useSharedState) — "what is the current value?" Convergent, hydrating, last-writer-wins. Use it for anything you'd render.
  • Messages (useMessage / useChannel) — "what just happened?" Fire-and-forget events with no history; a tab that joins later never sees old events. Use it for triggers: logged-out, cart-updated.
  • Presence (usePeers) — "who else is here?" A live list of peer { id, kind }, maintained by heartbeats.
The litmus test

If a late-joining tab must know it, it is state. If only currently-open tabs care, it is a message.

Choosing the blast radius

useSharedState lets you delimit how far a value travels:

Same key, different scopes = different values. Scope is part of the identity, on purpose — a tab-scoped 'draft' and an everywhere-scoped 'draft' never bleed into each other.

Where to next

  • Hooks overview — the API this model maps onto, hook by hook.
  • How sync works — version clocks, convergence, and the late-joiner handshake, step by step.
  • Security model — what the cross-origin channel validates and why.