Search selfstore
v1.8.21

Framework bindings

The whole binding contract is two members every store has: store.subscribe(fn) and store.state (with store.status and store.error on top). The state snapshot is referentially stable between changes, so equality-based frameworks re-render only when something actually changed. That is why there is no selfstore-react package: it would be three lines in a trench coat.

React

import { useSyncExternalStore } from 'react';

function useStatus() {
  useSyncExternalStore(store.subscribe, () => store.state, () => store.state);
  return store.status; // { state, severity, action, labelKey }
}

function SaveBadge() {
  const status = useStatus();
  return <span>{t(status.labelKey)}</span>;
}

The third argument matters: it is the server snapshot, and without it useSyncExternalStore throws during SSR (Next.js, Remix). Keep it.

Svelte

The store contract is structural, so this object is a Svelte readable, no svelte import needed, Svelte 3 through 5:

export const persistence = {
  subscribe(run: (s: typeof store.state) => void) {
    run(store.state);
    return store.subscribe(() => run(store.state));
  },
};
<span>{t($persistence.status.labelKey)}</span>

Vue

import { onMounted, onUnmounted, shallowRef } from 'vue';

export function usePersistence() {
  const state = shallowRef(store.state);
  let stop: (() => void) | undefined;
  onMounted(() => (stop = store.subscribe(() => (state.value = store.state))));
  onUnmounted(() => stop?.());
  return state;
}

Solid, Preact signals, Lit, vanilla: same shape. Subscribe on mount, read store.state, call the returned unsubscriber on unmount.

Two gestures, and teardown

store.status.action is set only when the user must act, and only to one of two values. Offer exactly that gesture:

if (store.status.action === 'unlock')    await store.unlock(await promptPassword());
if (store.status.action === 'reconnect') await store.reconnect();

Transient trouble (offline, a cold-started backend, a 5xx) never sets action: the edit stays safe locally and the next save or sync retries on its own.

In an SPA, call store.dispose() when a route-scoped or test-scoped store unmounts: it cancels timers and drops subscribers. An app-wide store needs nothing.