Shared state
Let's take shared state beyond the counter. In this guide we'll pick how far
a value travels, namespace keys so features can't collide, watch two tabs
fight over one key (and agree anyway), and reach the same state from code
that isn't React. If you want the raw API surface instead, that's
useSharedState.
Everything starts from one line:
const [note, setNote] = useSharedState('note', '');
useState with a bigger blast radius: the value lives in every tab, window,
and worker on your origin.
Choose how far a value travels
Not every value should reach everywhere. The scope option delimits the
blast radius:
useSharedState('draft', '', { scope: 'everywhere' }); // tabs + windows + workers (default)
useSharedState('draft', '', { scope: 'tabs' }); // ignore writes from workers
useSharedState('draft', '', { scope: 'tab' }); // this tab only
everywhere— synced across every context on the origin. The default, and right for most things.tabs— synced, but writes coming from workers are silently ignored. Useful when a worker feeds a store that the UI sometimes wants to override locally.tab— never leaves the tab, but still shared between every component in it. A zero-Provider way to share state within one page.
Each scope is an independent namespace: a tab-scoped 'draft' and an
everywhere-scoped 'draft' are different values. Scope is part of the
identity — nothing ever bleeds between scopes by accident.
Namespace keys with stores
Keys live inside a named store (default 'use-everywhere'). Give each
feature area its own store and stop thinking about key collisions:
const [step] = useSharedState('step', 0, { store: 'checkout' });
const [step2] = useSharedState('step', 0, { store: 'onboarding' }); // unrelated
Sharing an origin with code you don't control (micro-frontends, embedded
apps)? Prefix the store name — 'myapp:checkout' — because same origin +
same name = same bus, for everyone.
Watch a conflict resolve
Here's the scenario that scares people off cross-tab state, so let's walk
straight into it. Tab A and tab B both write note in the same millisecond:
| Moment | Tab A (aaaaaa) | Tab B (bbbbbb) |
|---|---|---|
| write | note = "from A" | note = "from B" |
| receive | B's write is "newer" (tie-break) → shows B | A's write is older → keeps B |
| result | "from B" | "from B" |
Every key carries a version clock; every peer applies the same deterministic
rule (higher counter wins, ties broken by client id); every tab lands on the
same value — with no coordinator and no extra round trips. A third tab
receiving the patches in either order also lands on "from B". The full
mechanics are in How sync works.
The honest cost: tab A's write was discarded, not merged. For flags, counters, form fields, wizard steps — exactly what you want. For two people typing in one document — wrong tool; that's CRDT territory. Writes to different keys never conflict at all, so splitting state across keys is the cheap way to avoid collisions entirely.
Trust the late joiner
Open a new tab mid-session and it renders the current state from its first paint. You don't write any code for this — but it's worth knowing why it works, because it's the part that kills the duplicate-tab-payment class of bug:
- The new tab broadcasts
hello; every existing peer answers with a snapshot of state and versions. - Your
initialvalue registers at version zero. - Anything a peer actually wrote has a higher version — so it beats your initial, always.
useSharedState('pay-status', 'idle') in a fresh tab hydrates to
'processing' if that's the truth out there. The initial value never stomps
a real one.
Reach the state from outside React
Workers, plain modules, event handlers outside components — the core engine is the same one the hooks use:
import { createSharedStore } from '@use-everywhere/core';
const store = createSharedStore('checkout', { step: 0 });
store.state.step++; // proxy writes sync everywhere
store.set('step', (prev) => prev + 1); // functional updates too
store.subscribe((key, value, meta) => {
console.log(key, value, meta.clientId, meta.self ? '(me)' : '(other tab)');
});
And in React code, getSharedStore(name, scope) returns the exact store
instance the hooks use — handy for patch logs and imperative writes:
import { getSharedStore, DEFAULT_NAME } from 'use-everywhere';
getSharedStore(DEFAULT_NAME).set('count', 0); // resets every tab's counter
Where to next
useSharedState— the full option and gotcha reference for the hook.- Recipes — the duplicate-tab lock, the live draft, and the worker engine, built from this page's pieces.
- How sync works — version clocks and handshakes, step by step.