nusm axolotlnusm
Core Concepts

Persistence Strategies

Persist the entire state object or isolate durable slices with explicit selectors and appliers.

Persistence is opt-in and belongs to each store, not to the application as a whole. Omit both adapter and persist for in-memory state. Add an adapter only when that store must cross a storage boundary; when configured, provide either a stable storeId or a non-empty Devtools name.

Required lifetimeStore configuration
Until reloadNo adapter; state stays in memory
Across reloads in the current tabcreateSessionStorageAdapter()
Across later browser sessionscreateLocalStorageAdapter() or createIndexDbAdapter()
Application-definedCustom adapter

These choices can coexist. For example, ephemeral UI state can use an in-memory nusm store while session evidence uses session storage and preferences use local storage.

Within one store, the slices strategy persists selected fields and leaves all other fields in memory. Use separate stores when values have separate ownership or lifecycles; use slices when they form one logical store.

Entire-store persistence

Use entire when the whole state object belongs to one persistence lifecycle.

const store = createNusmStore(
  {
    filters: { status: "all" },
    layout: "comfortable",
  },
  {
    adapter: createLocalStorageAdapter(),
    persist: { strategy: "entire" },
    storeId: "workspace-preferences",
  },
)

The default physical key is:

nusm:workspace-preferences:entire

On hydration, persisted data deep-merges over initial state. Arrays are replaced rather than concatenated. Supply hydrate.merge when your schema needs another policy.

Slice persistence

Use slices when only part of the store should survive, when fields need separate keys, or when unrelated updates should not schedule a write.

import type { PersistSlice } from "nusm"

type WorkspaceState = {
  draft: string
  filters: { status: "all" | "open" | "closed" }
  online: boolean
}

const slices: Array<PersistSlice<WorkspaceState>> = [
  {
    key: "filters",
    select: (state) => state.filters,
    apply: (state, value) => ({
      ...state,
      filters: value as WorkspaceState["filters"],
    }),
  },
]

const store = createNusmStore<WorkspaceState>(
  { draft: "", filters: { status: "all" }, online: true },
  {
    adapter: createLocalStorageAdapter(),
    persist: { slices, strategy: "slices" },
    storeId: "workspace",
  },
)

Only a change where Object.is(previousSelected, nextSelected) is false schedules that slice. The default key is nusm:workspace:slice:filters.

Slice invariants

  • Logical key values must be unique.
  • Adapter-resolved physical keys must be non-empty and unique.
  • apply must change only the slice represented by its matching select.
  • Multiple missing slices are validated together before baselines are written.
  • A clear event resets configured slices to their initial selected values while leaving non-persisted fields alone.

These constraints prevent one slice from silently changing another slice's durable baseline.

Write scheduling

Each adapter owns its pacing policy:

const adapter = createLocalStorageAdapter({
  pacer: {
    leading: false,
    trailing: true,
    wait: 100,
  },
})

Set pacer: false to start a write immediately for every scheduled persistence operation:

const adapter = createSessionStorageAdapter({ pacer: false })

The built-in defaults are 50 ms trailing for local and session storage, and 100 ms trailing for IndexedDB.

Key layout

Built-in adapters accept prefix where applicable. A custom adapter can take complete control with resolveKey:

resolveKey: ({ storeId, kind, sliceKey }) =>
  kind === "entire"
    ? `app:${storeId}`
    : `app:${storeId}:${sliceKey}`

nusm resolves each configured unit once and reuses that key for hydration, writes, external events, and Devtools commands.

On this page