@stainless-code/persist
Version:
Hydration-aware persistence for any reactive store — zero-dep persistSource core; codecs, backends, cross-tab transport, source + framework hydration adapters ship as opt-in recipes
99 lines (72 loc) • 4.09 kB
Markdown
---
name: persist-zustand
description: Persist a zustand StoreApi with @stainless-code/persist (persistStore). Use when wiring zustand to localStorage/sessionStorage/IndexedDB, replacing zustand/middleware persist, or choosing between persistStore and persistSource for a zustand store.
license: MIT
metadata:
type: composition
library: "@stainless-code/persist"
library_version: "0.4.1"
framework: "zustand"
sources:
- stainless-code/persist:README.md
- stainless-code/persist:docs/architecture.md
- stainless-code/persist:src/adapters/sources/zustand.ts
---
# Persisting zustand
`@stainless-code/persist/sources/zustand` maps zustand's `StoreApi` (`getState` / `setState` / `subscribe`) onto `persistSource`. Prefer this over zustand's built-in `persist` middleware when you want Persist's hydration gate, trailing throttle, cross-tab helpers, or the same storage/codec stack as other Persist adapters.
## When to use this skill
- You have a zustand store (`create(...)` / `StoreApi`) and want it to survive reload.
- You're migrating off `zustand/middleware`'s `persist`.
- You need async-backend hydration gating (`useHydrated`) shared with other Persist sources.
Other sources → matching `persist-*` skill. UI gate → `*-persist`.
## Install
```bash
bun add @stainless-code/persist zustand
# only when you use a codec that needs it:
bun add seroval
```
`zustand` is an optional peer of the `/sources/zustand` subpath — importing the subpath is the dep opt-in.
## Minimal wiring
```ts
import { create } from "zustand";
import { createJSONStorage } from "@stainless-code/persist";
import { persistStore } from "@stainless-code/persist/sources/zustand";
const usePrefs = create(() => ({ theme: "light" as const }));
const persist = persistStore(usePrefs, {
name: "app:prefs:v1",
storage: createJSONStorage(() => localStorage),
});
```
Pass the store API (`usePrefs`), not a hook call. `persist` is a `PersistApi` — keep it for `rehydrate()` / `destroy()` / `onHydrate` / `clearStorage()`.
## vs zustand/middleware `persist`
| Concern | zustand `persist` | `@stainless-code/persist` |
| ------------------ | -------------------- | ---------------------------------------------------- |
| Hydration gate | library-specific | built-in; pair with `useHydrated` for async backends |
| Storage / codecs | JSON-centric helpers | `createStorage` + json / seroval / identity / custom |
| Cross-tab | optional listeners | `crossTab` + `crossTabEventTarget` for IDB |
| Multi-library apps | zustand-only | same Options / `PersistApi` across source adapters |
Keep zustand middleware for zustand-only apps that already depend on its persist API. Switch when you want one persist stack across stores.
## Hydration gate
Writes are gated until hydration settles (same as other Persist sources). Sync backends settle in a microtask; IndexedDB needs a UI gate:
```ts
import { toHydrationSignal } from "@stainless-code/persist";
import { useHydrated } from "@stainless-code/persist/frameworks/react";
export const prefsHydration = toHydrationSignal(persist);
const { hydrated } = useHydrated(prefsHydration);
```
## Teardown — required for non-singletons
`persistStore` subscribes to the store. For route/component-scoped stores, `destroy()` on unmount.
```ts
useEffect(() => {
const persist = persistStore(store, opts);
return () => persist.destroy();
}, [store]);
```
## Common mistakes
- **Passing `usePrefs()` (state) instead of `usePrefs` (API).** The adapter needs `getState` / `setState` / `subscribe`.
- **Stacking zustand `persist` middleware and this adapter.** Pick one write path.
- **Gating writes manually before hydration.** The gate is built in.
- **`identityCodec` with string-only backends.** Use `jsonCodec` / `createJSONStorage` for `localStorage`; `identityCodec` is for structured-clone IDB.
## API surface for this skill
- `persistStore(store, options) → PersistApi` — `store: StoreApi<TState>`
- Options / `PersistApi`: see skill `persist`.