Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions apps/docs/content/guides/getting-started.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions apps/docs/content/guides/idb-react.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
89 changes: 52 additions & 37 deletions apps/docs/content/guides/migrating.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -73,30 +71,40 @@ 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";

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);
```

Expand All @@ -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);
```
4 changes: 3 additions & 1 deletion apps/docs/content/recipes/wrapping-stores.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
13 changes: 8 additions & 5 deletions apps/docs/pages/_home/Hero.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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";
---

<section class="text-center">
Expand Down Expand Up @@ -56,12 +55,16 @@ const defaultSource = "tanstack-store";
{
sourceSnippets.map((snippet) => (
<button
class={`inline-flex items-center gap-1.5 ${pillBase} ${snippet.id === defaultSource ? pillActive : pillMuted}`}
class={`inline-flex items-center gap-1.5 ${pillBase} ${snippet.id === defaultSourceId ? pillActive : pillMuted}`}
data-install-command={snippet.installCommand}
data-source={snippet.id}
type="button"
>
<Icon class="size-4 shrink-0" name={snippet.icon} size={16} />
<Icon
class="size-4 shrink-0 object-contain"
name={snippet.icon}
size={16}
/>
{snippet.label}
</button>
))
Expand All @@ -72,7 +75,7 @@ const defaultSource = "tanstack-store";
{
sourceSnippets.map((snippet) => (
<div
class={snippet.id === defaultSource ? undefined : "hidden"}
class={snippet.id === defaultSourceId ? undefined : "hidden"}
data-code={snippet.id}
>
<CodeBlock code={snippet.code} lang={snippet.lang} />
Expand Down
6 changes: 5 additions & 1 deletion apps/docs/pages/_home/InstallBox.astro
Original file line number Diff line number Diff line change
@@ -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";
---

<div
Expand Down
8 changes: 4 additions & 4 deletions apps/docs/pages/_home/Seams.astro
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ const codecs = ["JSON", "seroval", "zod", "identity"];

const sources = [
"TanStack Store",
"zustand",
"jotai",
"valtio",
"mobx",
"Zustand",
"Jotai",
"Valtio",
"MobX",
"custom source",
];

Expand Down
41 changes: 21 additions & 20 deletions apps/docs/pages/_home/source-snippets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,30 @@ export interface SourceSnippet {
code: string;
}

export const defaultSourceId = "tanstack-store";

/** Order: tanstack-store → zustand → jotai → valtio → mobx → custom */
export const sourceSnippets: SourceSnippet[] = [
{
id: "tanstack-store",
label: "TanStack Store",
icon: "database",
icon: "/brands/tanstack.svg",
lang: "ts",
installCommand:
"bun add @stainless-code/persist @tanstack/store idb-keyval seroval",
installCommand: "bun add @stainless-code/persist @tanstack/store",
code: `import { Store } from "@tanstack/store";
import { createSerovalStorage } from "@stainless-code/persist/codecs/seroval";
import { createJSONStorage } from "@stainless-code/persist";
import { persistStore } from "@stainless-code/persist/sources/tanstack-store";
import { toHydrationSignal } from "@stainless-code/persist";

const store = new Store({ theme: "light" });
const persist = persistStore(store, {
persistStore(store, {
name: "app:prefs:v1",
storage: createSerovalStorage(() => 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";
Expand All @@ -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";
Expand All @@ -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";
Expand All @@ -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";
Expand All @@ -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",
Expand Down
Binary file added apps/docs/public/brands/jotai.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/docs/public/brands/mobx.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading