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 | 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 ​

Released under the MIT License.