Skip to content
Persist
Esc
navigateopen⌘Jpreview
On this page

Wrapping stores

Persist zustand, jotai, valtio, mobx, pinia, redux, or any custom PersistableSource with thin source adapters.

Same getState/setState/subscribe shape → same adapter name → same merge semantics, regardless of library. Naming is shape-based (persistStore / persistAtom / persistProxy / persistObservable); the subpath carries the library. Importing two same-shape adapters into one module? Alias one: import { persistStore as persistZustand } from "@stainless-code/persist/sources/zustand". Escape hatch: pass a custom PersistableSource to persistSource directly.

zustand

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),
});

Or pass a custom PersistableSource to persistSource directly — the adapter is a thin wrapper over getState/setState/subscribe.

jotai

import { atom, createStore } from "jotai";
import { createJSONStorage } from "@stainless-code/persist";
import { persistAtom } from "@stainless-code/persist/sources/jotai";

const store = createStore();
const themeAtom = atom<"light" | "dark">("light");
const persist = persistAtom(store, themeAtom, {
  name: "app:theme:v1",
  storage: createJSONStorage(() => localStorage),
});

Or pass a custom PersistableSource to persistSource directly — the adapter is a thin wrapper over getState/setState/subscribe.

valtio

import { proxy } from "valtio";
import { createJSONStorage } from "@stainless-code/persist";
import { persistProxy } from "@stainless-code/persist/sources/valtio";

const prefs = proxy({ theme: "light" as const });
const persist = persistProxy(prefs, {
  name: "app:prefs:v1",
  storage: createJSONStorage(() => localStorage),
});

Or pass a custom PersistableSource to persistSource directly — the adapter is a thin wrapper over getState/setState/subscribe.

mobx

import { observable } from "mobx";
import { createJSONStorage } from "@stainless-code/persist";
import { persistObservable } from "@stainless-code/persist/sources/mobx";

const prefs = observable.object({ theme: "light" as const });
const persist = persistObservable(prefs, {
  name: "app:prefs:v1",
  storage: createJSONStorage(() => localStorage),
});

Or pass a custom PersistableSource to persistSource directly — the adapter is a thin wrapper over getState/setState/subscribe.

pinia

import { defineStore } from "pinia";
import { createJSONStorage } from "@stainless-code/persist";
import { persistStore } from "@stainless-code/persist/sources/pinia";

const usePrefs = defineStore("prefs", {
  state: () => ({ theme: "light" as const }),
});
const persist = persistStore(usePrefs(), {
  name: "app:prefs:v1",
  storage: createJSONStorage(() => localStorage),
});

redux

Wrap the root reducer with persistableReducer (not each slice) so hydrate can replace state. Alias if you also import redux-persist’s persistStore.

import { createStore } from "redux";
import { createJSONStorage } from "@stainless-code/persist";
import {
  persistStore,
  persistableReducer,
} from "@stainless-code/persist/sources/redux";

const store = createStore(persistableReducer(rootReducer));
const persist = persistStore(store, {
  name: "app:root:v1",
  storage: createJSONStorage(() => localStorage),
});

Any other store

import { createJSONStorage, persistSource } from "@stainless-code/persist";

const persist = persistSource(
  {
    getState: () => myStore.getState(),
    setState: (updater) => myStore.setState(updater),
    subscribe: (listener) => ({
      unsubscribe: myStore.subscribe(() => listener()),
    }),
  },
  {
    name: "app:custom:v1",
    storage: createJSONStorage(() => localStorage),
  },
);

Last updated on July 20, 2026

Was this page helpful?