Persistence
Persistence is opt-in and whitelist-first. You choose which paths to persist and which storage adapter to use.
Quick Setup
ts
mesh.persist({
storage: "localStorage",
keys: ["theme", "cart.items"],
version: 1,
ttl: "7d"
});| Option | Type | Default | Description |
|---|---|---|---|
storage | "localStorage" | "sessionStorage" | "indexedDB" | "memory" | "localStorage" | Storage adapter |
keys | string[] | Required | State paths to persist |
key | string | Mesh name | Storage key prefix |
version | number | 1 | Schema version for migration |
ttl | string | number | — | Time-to-live. "7d", "1h", "30m", or ms. |
Storage Adapters
localStorage
ts
import { createLocalStorageAdapter } from "statemesh-core/persist";
mesh.persist({
storage: createLocalStorageAdapter(),
keys: ["theme"]
});sessionStorage
ts
import { createSessionStorageAdapter } from "statemesh-core/persist";
mesh.persist({
storage: createSessionStorageAdapter(),
keys: ["theme"]
});IndexedDB
ts
import { createIndexedDBStorageAdapter } from "statemesh-core/persist";
mesh.persist({
storage: createIndexedDBStorageAdapter({ dbName: "my-app" }),
keys: ["cart", "preferences"]
});Memory (for testing/SSR)
ts
import { createMemoryStorageAdapter } from "statemesh-core/persist";
mesh.persist({
storage: createMemoryStorageAdapter("test-store"),
keys: ["theme"]
});Version Migration
When the schema version changes, run migrations:
ts
import { createMigration } from "statemesh-core/persist";
mesh.persist({
storage: "localStorage",
keys: ["settings"],
version: 3,
migrations: {
2: createMigration((state) => {
// v1 → v2: rename field
state.settings.theme = state.settings.colorMode;
delete state.settings.colorMode;
}),
3: createMigration((state) => {
// v2 → v3: add default
state.settings.fontSize ??= 16;
})
}
});TTL Expiration
Data older than the TTL is discarded on load:
ts
mesh.persist({
storage: "localStorage",
keys: ["session"],
ttl: "24h" // Expire after 24 hours
});Custom Serializer
ts
import { jsonSerializer } from "statemesh-core/persist";
mesh.persist({
storage: "localStorage",
keys: ["data"],
serializer: jsonSerializer // Default
});Resource Cache Persistence
Persist resource cache separately from app state:
ts
mesh.persistResources({
key: "app:resources",
storage: "localStorage",
names: ["products.list"],
ttl: "10m"
});Full Dehydrate/Hydrate
For SSR, tests, or full app restore:
ts
// Snapshot everything
const snapshot = mesh.dehydrate({
forms: true,
resources: true,
queuedMutations: true
});
// Restore everything
mesh.hydrate(snapshot, {
mergeState: true,
resources: true,
queuedMutations: true
});SSR Pattern
ts
// Server: dehydrate and pass to client
const snapshot = mesh.dehydrate();
res.send(`<script>window.__STATEMESH__ = ${JSON.stringify(snapshot)}</script>`);
// Client: hydrate on mount
mesh.hydrate(window.__STATEMESH__);Important Notes
TIP
StateMesh only writes persistence again when one of the whitelisted paths changes. Unrelated state updates do not touch storage.
WARNING
Corrupted persisted data is ignored instead of crashing the app. If deserialization fails, the initial state is used.
TIP
IndexedDB is async. If you need synchronous access (e.g., for SSR), use localStorage or memory adapters.
Next Steps
- Resources — Cached API reads
- Mutations — API writes with offline queueing
- Cross-Tab Sync — Sync state across browser tabs
