diff --git a/apps/docs/content/guides/getting-started.mdx b/apps/docs/content/guides/getting-started.mdx index b47135d..7a11ce5 100644 --- a/apps/docs/content/guides/getting-started.mdx +++ b/apps/docs/content/guides/getting-started.mdx @@ -9,15 +9,16 @@ You already have a store. You need durable state without a first-paint flash — ## Install -```bash -bun add @stainless-code/persist +```package-install +npm i @stainless-code/persist ``` Core is zero-dep. Each subpath is an optional peer — install only what you import. Full table: [Entry points](/concepts/entry-points). -```bash -# peers for the quick-start imports below -bun add seroval idb-keyval @tanstack/store react +Peers for the quick-start imports below: + +```package-install +npm i seroval idb-keyval @tanstack/store react ``` ## Quick start diff --git a/apps/docs/content/guides/idb-react.mdx b/apps/docs/content/guides/idb-react.mdx index 558f5da..88431e2 100644 --- a/apps/docs/content/guides/idb-react.mdx +++ b/apps/docs/content/guides/idb-react.mdx @@ -7,8 +7,8 @@ search: IndexedDB reads are Promise-backed — they cannot settle before first paint. This guide wires `createIdbStorage`, TanStack Store, and a React hydration gate. -```bash -bun add @stainless-code/persist @tanstack/store react idb-keyval +```package-install +npm i @stainless-code/persist @tanstack/store react idb-keyval ``` **Store module** — `createIdbStorage` runs structured-clone mode: `Set` / `Map` / `Date` round-trip natively, no codec. The persist + hydration signal live at module scope for an app-lifetime singleton store. diff --git a/apps/docs/content/guides/migrating.mdx b/apps/docs/content/guides/migrating.mdx index 64991d7..16a2f08 100644 --- a/apps/docs/content/guides/migrating.mdx +++ b/apps/docs/content/guides/migrating.mdx @@ -37,19 +37,17 @@ export const usePrefs = create( }), ); -// @stainless-code/persist -import { Store } from "@tanstack/store"; +// @stainless-code/persist — same zustand store, adapter instead of middleware +import { create } from "zustand"; import { createJSONStorage, toHydrationSignal } from "@stainless-code/persist"; -import { persistStore } from "@stainless-code/persist/sources/tanstack-store"; -import { useHydrated } from "@stainless-code/persist/frameworks/react"; +import { persistStore } from "@stainless-code/persist/sources/zustand"; -export const prefsStore = new Store({ theme: "light" }); -const persist = persistStore(prefsStore, { +export const usePrefs = create(() => ({ theme: "light" })); +const persist = persistStore(usePrefs, { name: "app:prefs:v1", storage: createJSONStorage(() => localStorage), }); export const prefsHydration = toHydrationSignal(persist); -// const { hydrated } = useHydrated(prefsHydration); ``` ## Migrating from redux-persist @@ -73,6 +71,7 @@ redux-persist reconciles whole reducer trees implicitly; here `merge` is explici ```ts // redux-persist +import { createStore } from "redux"; import { persistReducer, persistStore } from "redux-persist"; import storage from "redux-persist/lib/storage"; @@ -80,23 +79,32 @@ const persistedReducer = persistReducer({ key: "root", storage }, rootReducer); export const store = createStore(persistedReducer); persistStore(store); -// @stainless-code/persist — wrap the redux store (dispatch ↔ setState) +// @stainless-code/persist — plain store + PersistableSource (dispatch ↔ setState) +import { createStore } from "redux"; import { createJSONStorage, persistSource, toHydrationSignal, } from "@stainless-code/persist"; -const reduxSource = { - getState: () => store.getState(), - setState: (updater) => - store.dispatch({ type: "PERSIST_SET", payload: updater(store.getState()) }), - subscribe: (listener) => store.subscribe(listener), -}; -const persist = persistSource(reduxSource, { - name: "root", - storage: createJSONStorage(() => localStorage), -}); +export const store = createStore(rootReducer); // rootReducer handles PERSIST_SET +const persist = persistSource( + { + getState: () => store.getState(), + setState: (updater) => + store.dispatch({ + type: "PERSIST_SET", + payload: updater(store.getState()), + }), + subscribe: (listener) => ({ + unsubscribe: store.subscribe(listener), + }), + }, + { + name: "root", + storage: createJSONStorage(() => localStorage), + }, +); export const rootHydration = toHydrationSignal(persist); ``` @@ -118,36 +126,43 @@ pinia-persist is a Pinia plugin; here persistence is a middleware call on any re ```ts // pinia-persist -import { createPinia } from "pinia"; +import { createPinia, defineStore } from "pinia"; import piniaPluginPersistedstate from "pinia-plugin-persistedstate"; const pinia = createPinia(); -pinia.use( - piniaPluginPersistedstate({ - key: "prefs", - storage: localStorage, - paths: ["theme"], - }), -); +pinia.use(piniaPluginPersistedstate); +export const usePrefsStore = defineStore("prefs", { + state: () => ({ theme: "light", draft: "" }), + persist: { key: "prefs", paths: ["theme"] }, +}); // @stainless-code/persist — wrap $state / $subscribe +import { defineStore } from "pinia"; import { createJSONStorage, persistSource, toHydrationSignal, } from "@stainless-code/persist"; -const piniaSource = { - getState: () => piniaStore.$state, - setState: (updater) => { - piniaStore.$state = updater(piniaStore.$state); - }, - subscribe: (listener) => piniaStore.$subscribe(() => listener()), -}; -const persist = persistSource(piniaSource, { - name: "prefs", - storage: createJSONStorage(() => localStorage), - partialize: (s) => ({ theme: s.theme }), +export const usePrefsStore = defineStore("prefs", { + state: () => ({ theme: "light", draft: "" }), }); +const prefsStore = usePrefsStore(); +const persist = persistSource( + { + getState: () => prefsStore.$state, + setState: (updater) => { + prefsStore.$patch(updater(prefsStore.$state)); + }, + subscribe: (listener) => ({ + unsubscribe: prefsStore.$subscribe(() => listener()), + }), + }, + { + name: "prefs", + storage: createJSONStorage(() => localStorage), + partialize: (s) => ({ theme: s.theme }), + }, +); export const prefsHydration = toHydrationSignal(persist); ``` diff --git a/apps/docs/content/recipes/wrapping-stores.mdx b/apps/docs/content/recipes/wrapping-stores.mdx index 49ae74b..78bb9e3 100644 --- a/apps/docs/content/recipes/wrapping-stores.mdx +++ b/apps/docs/content/recipes/wrapping-stores.mdx @@ -81,7 +81,9 @@ const persist = persistSource( { getState: () => myStore.getState(), setState: (updater) => myStore.setState(updater), - subscribe: (listener) => myStore.subscribe(() => listener()), + subscribe: (listener) => ({ + unsubscribe: myStore.subscribe(() => listener()), + }), }, { name: "app:custom:v1", diff --git a/apps/docs/pages/_home/Hero.astro b/apps/docs/pages/_home/Hero.astro index fdc7e56..1c80810 100644 --- a/apps/docs/pages/_home/Hero.astro +++ b/apps/docs/pages/_home/Hero.astro @@ -3,14 +3,13 @@ import CodeBlock from "blume/components/content/CodeBlock.astro"; import Icon from "blume/components/Icon.astro"; import { contentHref } from "blume/components/content/base-href.ts"; import InstallBox from "./InstallBox.astro"; -import { sourceSnippets } from "./source-snippets.ts"; +import { defaultSourceId, sourceSnippets } from "./source-snippets.ts"; const pillBase = "cursor-pointer rounded-full border px-3 py-1 text-xs transition active:scale-[0.97] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"; const pillActive = "border-accent bg-accent/10 font-medium text-accent"; const pillMuted = "border-border text-muted-foreground hover:border-foreground/40 hover:text-foreground"; -const defaultSource = "tanstack-store"; ---
@@ -56,12 +55,16 @@ const defaultSource = "tanstack-store"; { sourceSnippets.map((snippet) => ( )) @@ -72,7 +75,7 @@ const defaultSource = "tanstack-store"; { sourceSnippets.map((snippet) => (
diff --git a/apps/docs/pages/_home/InstallBox.astro b/apps/docs/pages/_home/InstallBox.astro index cfce402..e7633a2 100644 --- a/apps/docs/pages/_home/InstallBox.astro +++ b/apps/docs/pages/_home/InstallBox.astro @@ -1,7 +1,11 @@ --- import Icon from "blume/components/Icon.astro"; +import { defaultSourceId, sourceSnippets } from "./source-snippets.ts"; -const installCommand = "bun add @stainless-code/persist"; +// First paint — Hero's click handler swaps the command later. +const installCommand = + sourceSnippets.find((s) => s.id === defaultSourceId)?.installCommand ?? + "bun add @stainless-code/persist"; ---
localStorage), -}); -export const prefsHydration = toHydrationSignal(persist);`, + storage: createJSONStorage(() => localStorage), +});`, }, { id: "zustand", - label: "zustand", - icon: "boxes", + label: "Zustand", + icon: "/brands/zustand.png", lang: "ts", installCommand: "bun add @stainless-code/persist zustand", code: `import { create } from "zustand"; @@ -46,8 +45,8 @@ persistStore(usePrefs, { }, { id: "jotai", - label: "jotai", - icon: "atom", + label: "Jotai", + icon: "/brands/jotai.png", lang: "ts", installCommand: "bun add @stainless-code/persist jotai", code: `import { atom, createStore } from "jotai"; @@ -63,8 +62,8 @@ persistAtom(store, themeAtom, { }, { id: "valtio", - label: "valtio", - icon: "activity", + label: "Valtio", + icon: "/brands/valtio.svg", lang: "ts", installCommand: "bun add @stainless-code/persist valtio", code: `import { proxy } from "valtio"; @@ -79,8 +78,8 @@ persistProxy(prefs, { }, { id: "mobx", - label: "mobx", - icon: "network", + label: "MobX", + icon: "/brands/mobx.svg", lang: "ts", installCommand: "bun add @stainless-code/persist mobx", code: `import { observable } from "mobx"; @@ -103,9 +102,11 @@ persistObservable(prefs, { persistSource( { - getState: () => myStore.get(), - setState: (next) => myStore.set(next), - subscribe: (listener) => myStore.subscribe(listener), + getState: () => myStore.getState(), + setState: (updater) => myStore.setState(updater), + subscribe: (listener) => ({ + unsubscribe: myStore.subscribe(() => listener()), + }), }, { name: "app:prefs:v1", diff --git a/apps/docs/public/brands/jotai.png b/apps/docs/public/brands/jotai.png new file mode 100644 index 0000000..58fce5d Binary files /dev/null and b/apps/docs/public/brands/jotai.png differ diff --git a/apps/docs/public/brands/mobx.svg b/apps/docs/public/brands/mobx.svg new file mode 100644 index 0000000..e533514 --- /dev/null +++ b/apps/docs/public/brands/mobx.svg @@ -0,0 +1 @@ + diff --git a/apps/docs/public/brands/tanstack.svg b/apps/docs/public/brands/tanstack.svg new file mode 100644 index 0000000..9c596b7 --- /dev/null +++ b/apps/docs/public/brands/tanstack.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/docs/public/brands/valtio.svg b/apps/docs/public/brands/valtio.svg new file mode 100644 index 0000000..a476bc6 --- /dev/null +++ b/apps/docs/public/brands/valtio.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/docs/public/brands/zustand.png b/apps/docs/public/brands/zustand.png new file mode 100644 index 0000000..c458b05 Binary files /dev/null and b/apps/docs/public/brands/zustand.png differ