Skip to main content

defineStore

Shared state is per-origin, but it is not permanent: close every tab and it is gone, because the value only ever lived in memory. defineStore gives a store a disk, so it comes back.

import { defineStore, localStorageAdapter } from 'use-everywhere';

const settings = defineStore<{ theme: string }>('settings', {
persist: localStorageAdapter('app:settings'),
});

function ThemeToggle() {
const [theme, setTheme] = settings.useSharedState('theme', 'light');
return <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>{theme}</button>;
}

Call it at module level, like defineChannel. It doesn't construct anything — it registers how the store should be built when something first needs it, so importing the module has no side effect.

Reopen the app and the theme is there on the first paint. No flash of 'light'.

Why the initial doesn't win

You passed 'light' as the initial and the stored value is 'dark', so why doesn't the initial clobber it?

Because the store is hydrated before any hook can register a key, and registerKey is a no-op for a key that already exists. The restored value is simply already there by the time your component asks.

It's the same store

defineStore doesn't create a private store. It resolves to the same singleton that a bare hook reaches, so these two touch one store, and both get persistence:

const settings = defineStore('settings', { persist: localStorageAdapter('app:settings') });

// elsewhere, no import of `settings` at all:
const [theme] = useSharedState('theme', 'light', { store: 'settings' });

If defineStore runs after that store already exists, it throws. Handing you back a store that silently isn't persisted is exactly the bug this design exists to prevent — so move the call to module scope, where it belongs.

Adapters

localStorageAdapter('key'); // survives closing every tab
sessionStorageAdapter('key'); // survives reloads, dies with the tab

Both degrade to a silent no-op if storage is unavailable — a sandboxed iframe, third-party cookies disabled, a full quota, or a corrupt entry from an older version of your app. Persistence is best-effort and must never be the thing that breaks your page.

Bring your own by implementing PersistAdapter:

interface PersistAdapter {
read(): Persisted | undefined | Promise<Persisted | undefined>;
write(snapshot: Persisted): void | Promise<void>;
remove?(): void | Promise<void>;
}

Prefer a synchronous read. An async adapter can't hydrate before your components render, so a write made in that gap can be overwritten by the restore.

Options

defineStore('settings', {
persist: localStorageAdapter('app:settings'),
persistKeys: ['theme'], // persist only these keys
persistDebounceMs: 100, // coalesce writes
scope: 'everywhere', // as in useSharedState
});

What actually goes to disk

The values and their version clocks. That's the part that matters.

A stored value carries the version it had, so a reopened tab re-enters the last-writer-wins race with its real term instead of a fresh zero. Concretely: if a live tab has moved the value on since you last closed, the live tab wins and your restored value is discarded. If the live tab is staler than what's on disk, the restored value wins and the live tab updates to match. Either way, every tab converges — you never end up with two tabs disagreeing.

Keys that were only ever registered — someone's initial, never written — are not persisted. There is nothing to save.