Skip to main content

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:

MomentTab A (aaaaaa)Tab B (bbbbbb)
writenote = "from A"note = "from B"
receiveB's write is "newer" (tie-break) → shows BA'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:

  1. The new tab broadcasts hello; every existing peer answers with a snapshot of state and versions.
  2. Your initial value registers at version zero.
  3. 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:

anywhere.ts
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.