Skip to content

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"
});
OptionTypeDefaultDescription
storage"localStorage" | "sessionStorage" | "indexedDB" | "memory""localStorage"Storage adapter
keysstring[]RequiredState paths to persist
keystringMesh nameStorage key prefix
versionnumber1Schema version for migration
ttlstring | numberTime-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

Released under the MIT License.