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.