---
title: "useSharedState"
description: "useState whose value lives in every tab, window and worker on your origin — converging everywhere without a server or a Provider."
source: https://rxova.org/packages/use-everywhere/hooks/use-shared-state/
---

# useSharedState

`useSharedState` is `useState` with a bigger blast radius: the value lives in
every tab, window, and worker on your origin, writes converge everywhere
within milliseconds, and a tab opened later hydrates to the current value
instead of the initial one.

```tsx
import { useSharedState } from 'use-everywhere';

function Counter() {
  const [count, setCount] = useSharedState('count', 0);
  return <button onClick={() => setCount((c) => c + 1)}>{count}</button>;
}
```

Render that in five tabs and click any button: all five update. Underneath
it's a versioned patch, a broadcast, and a deterministic merge rule on every
peer — the details live in
[How sync works](https://rxova.org/packages/use-everywhere/under-the-hood/how-sync-works.md), but you don't need
them to use it.

## Signature

```ts
function useSharedState<T>(
  key: string,
  initial: T,
  options?: UseSharedStateOptions,
): [T, (next: T | ((prev: T) => T)) => void];
```

Exactly the `useState` tuple: the current value, and a setter that accepts
either a value or an updater function.

## Options

| Option  | Type                              | Default            | What it does                                                                      |
| ------- | --------------------------------- | ------------------ | --------------------------------------------------------------------------------- |
| `store` | `string`                          | `'use-everywhere'` | Which named store the key lives in — a namespace, so feature areas can't collide. |
| `scope` | `'everywhere' \| 'tabs' \| 'tab'` | `'everywhere'`     | How far the value travels (see below).                                            |

### `store` — namespacing your keys

Keys live inside a named store. Two features can both use a `'step'` key
without ever colliding, as long as they use different stores:

```tsx
const [step] = useSharedState('step', 0, { store: 'checkout' });
const [step2] = useSharedState('step', 0, { store: 'onboarding' }); // unrelated value
```

If you don't control the whole origin (micro-frontends, embedded widgets),
prefix your store names — `'myapp:cart'` — so another app's `'cart'` can't
interfere.

### `scope` — choosing the blast radius

```tsx
useSharedState('draft', '', { scope: 'everywhere' }); // tabs + windows + workers (default)
useSharedState('draft', '', { scope: 'tabs' }); // ignore writes coming from workers
useSharedState('draft', '', { scope: 'tab' }); // this tab only
```

- **`everywhere`** — synced across every context on the origin.
- **`tabs`** — still synced across tabs and windows, but patches originating
  from workers are silently dropped. Useful when a worker feeds data you
  _sometimes_ don't want.
- **`tab`** — a no-op transport: nothing leaves, nothing arrives. Every
  component in the tab using that key still shares one value, so it's a
  zero-Provider way to share state within a tab.

One thing to internalize: **scope is part of the identity.** The same key in
`'tab'` and `'everywhere'` scopes is two different values, on purpose.

## Worked example: a payment status every tab agrees on

The duplicate-tab payment bug, solved with one hook:

```tsx title="PayButton.tsx"
import { useSharedState } from 'use-everywhere';

type PayStatus = 'idle' | 'processing' | 'paid';

function PayButton() {
  const [status, setStatus] = useSharedState<PayStatus>('pay-status', 'idle', {
    store: 'checkout',
  });

  const pay = async () => {
    setStatus('processing'); // every tab's button disables right now
    await chargeCard();
    setStatus('paid'); // every tab shows the receipt state
  };

  return (
    <button onClick={pay} disabled={status !== 'idle'}>
      {status === 'idle' ? 'Pay' : status === 'processing' ? 'Processing…' : 'Paid ✓'}
    </button>
  );
}
```

The part that matters: a tab opened _while_ the payment is processing renders
`'processing'` from its very first frame — not `'idle'` — because its initial
value never overrides a value another tab actually wrote. That's the whole
class of bug, gone. (For the full pattern with an owner id, see the
[single-flight recipe](https://rxova.org/packages/use-everywhere/guides/recipes.md#the-duplicate-tab-lock-single-flight).)

## Gotchas

- **Initial values never win over real writes.** Your `initial` registers at
  version zero; anything a peer actually wrote has a higher version and beats
  it. First mount registers the initial, later mounts reuse it — so two tabs
  with different initials converge deterministically too.
- **Concurrent writes to the same key: one wins, one is discarded.**
  Last-writer-wins with a deterministic tie-break. Perfect for flags, form
  fields, and drafts; wrong for collaborative text editing — that's
  [CRDT territory](https://rxova.org/packages/use-everywhere/under-the-hood/limitations.md#last-writer-wins-loses-concurrent-writes).
- **The rule is per key, not per operation — so counters can lose an
  increment.** `setCount((c) => c + 1)` applies the updater to _this_ tab's
  value and broadcasts the result. Two tabs incrementing 5 at the same instant
  both compute 6, and 6 is what every tab converges on: the tabs agree, and one
  `+1` is gone. It converges, it just does not accumulate. When the total has
  to be right, do the counting in one tab
  ([`useLeaderEffect`](https://rxova.org/packages/use-everywhere/hooks/use-leader.md)) or on your server.
- **Values must survive structured clone.** Plain objects, arrays, `Map`,
  `Set`, `Date`, typed arrays: fine. Functions, class instances, DOM nodes,
  React elements: not fine — writing one throws, and the write does not
  happen, so your tab never diverges from its peers.
- **One key, one default.** The first `useSharedState('k', …)` to run
  registers the default; a second call elsewhere passing a different one is
  ignored. Development warns when it spots the disagreement.
- **Replace values, don't mutate them.** Syncing happens on assignment, not
  in-place mutation: `setCart({ ...cart, items: 3 })` syncs;
  `cart.items = 3` on a value you read does not. Development deep-freezes
  shared values, so that mistake throws instead of failing silently —
  [details](https://rxova.org/packages/use-everywhere/guides/shared-state.md#replace-values-dont-mutate-them).
- **Not persisted by default.** Close the last tab and the value is gone.
  Opt in with [`createStoreHooks`'s `persist`](https://rxova.org/packages/use-everywhere/hooks/create-store-hooks.md) to keep it — with
  its version clocks, so it re-hydrates correctly.
- **Subscriptions are per key.** Components using the same key (and store and
  scope) share one subscription, and a `note` update never re-renders `count`
  consumers.
- **SSR renders `initial`.** On the server the hook returns your initial
  value; after hydration the client converges to the shared value.

## Outside React

`getSharedStore(name?, scope?)` returns the exact store instance the hook
uses — handy for patch logs, event handlers outside components, or writes
from non-React code:

```ts
import { getSharedStore, DEFAULT_NAME } from 'use-everywhere';

const store = getSharedStore(DEFAULT_NAME);
store.set('count', (prev) => (prev ?? 0) + 1);
store.subscribe((key, value, meta) => console.log(key, value, meta.clientId));
```

## Where to next

- [Shared state guide](https://rxova.org/packages/use-everywhere/guides/shared-state.md) — scopes, stores, and
  conflict behavior as a walkthrough.
- [How sync works](https://rxova.org/packages/use-everywhere/under-the-hood/how-sync-works.md) — the version clocks
  and handshakes underneath.
- [`useOnMessage`](https://rxova.org/packages/use-everywhere/hooks/use-on-message.md) — for things that _happen_ rather than
  things that _are_.
