Skip to content
Persist
Esc
navigateopen⌘Jpreview

Any store. Any storage.One middleware.

Stop rewriting persist glue when you swap zustand for TanStack Store — or localStorage for IndexedDB.

Zero-deppersistSourcecore; codecs, backends, transport, and framework gates are opt-in subpaths.

3
seams
zero-dep
core
opt-in
peers
import { Store } from "@tanstack/store";
import { createJSONStorage } from "@stainless-code/persist";
import { persistStore } from "@stainless-code/persist/sources/tanstack-store";

const store = new Store({ theme: "light" });
persistStore(store, {
  name: "app:prefs:v1",
  storage: createJSONStorage(() => localStorage),
});
bun add @stainless-code/persist @tanstack/store

When to use it

When to reach for Persist.

Client store state is durable and cross-cutting, but most persist glue is store-locked. Persist binds to a structural source and a hydration lifecycle you can gate from any framework.

Use caseWhat it involvesFit
Persist prefs / draft state across reloads without a hydrate flashtoHydrationSignal + framework useHydratedIdeal
Same persist middleware across zustand, TanStack Store, jotai, …PersistableSource + ./sources/*Ideal
Swap localStorage ↔ IndexedDB ↔ encrypted wrapper without rewritecreateStorage(backend, codec) seamsIdeal
Versioned schema evolution for long-lived keysversion + migrate / createMigrationChainIdeal
Keep tabs in sync (or BroadcastChannel over IDB)crossTab + ./transport/crosstabIdeal
Survive quota errors without clobbering newer writesretryWrite (shrink-or-give-up + generation guard)Ideal
Schema-gate prefs or round-trip Date/Map/Set./codecs/standard-schema · ./codecs/serovalGood
Clear every registered key on logoutcreatePersistRegistry + clearAllNice-to-have
Gate UI the same way in React / Preact / Solid / Angular / Vue / Lit / Alpine / Svelte./frameworks/* hydration adaptersIdeal

When to skip it.

Persist pays for itself when you need a hydration signal, swappable seams, or store-agnostic middleware. A one-off sync write is cheaper as plain storage.

Use caseFit
A single sync localStorage.setItem with no hydrate gate, migrate, or multi-tab needsSkip → plain storage helpers
Persisting a query/cache layer (TanStack Query, SWR, …) — different lifecycleSkip → that library's persist client
Server-authoritative state with no client durable copySkip → your API + cache policy

Three seams. Swap any cell.

Backend × codec × source compose throughcreateStorageandpersistSource— framework adapters only mount the hydration signal.

Backends

  • localStorage / session
  • IndexedDB
  • AsyncStorage
  • MMKV
  • Secure Store
  • Node fs
  • Encrypted
  • Compressed

Codecs

  • JSON
  • seroval
  • Standard Schema
  • identity

Sources

  • TanStack Store
  • Zustand
  • Jotai
  • Valtio
  • MobX
  • Pinia
  • Redux
  • Any source

Frameworks

  • React
  • Preact
  • Solid
  • Angular
  • Vue
  • Lit
  • Alpine
  • Svelte (runes)
  • Svelte (store)
ConcernWhere it lives
Wire format / validation./codecs/*
Durable key/value IO./backends/* · core factories
Store shape → PersistableSource./sources/*
Hydration gate in UI./frameworks/*
Multi-tab over async backends./transport/crosstab

Batteries included.

Handle the persist cases that stop being local: async hydrate flash, schema evolution, quota retries, multi-tab sync, and rich serialization. Import only the subpaths you need.

Start persisting without the flash.

Install the core, then add peers only for the subpaths you import.

bun add @stainless-code/persist @tanstack/store