Stately

Persist

The persist extension saves and restores store state from storage. It supports localStorage, sessionStorage, and async adapters like React Native's AsyncStorage.

import { createStore } from '@xstate/store';
import { persist } from '@xstate/store/persist'; 

const store = createStore({
  context: { count: 0 },
  on: {
    inc: (context) => ({ count: context.count + 1 }),
  },
}).with(
  persist({ name: 'counter' }),
);

State is automatically saved to localStorage on each event and restored on store creation.

Strategies

Snapshot strategy (default)

Persists the full context on every event. Simple, predictable.

.with(persist({
  name: 'my-store',
  strategy: 'snapshot', // default
}))

Event strategy

Persists the event log and replays events on hydration. Useful for event sourcing or audit trails.

.with(persist({
  name: 'my-store',
  strategy: 'event', 
  maxEvents: 100,
}))

When maxEvents is exceeded, a snapshot checkpoint is saved and the oldest events are dropped. Replay starts from the checkpoint.

Options

Shared options

OptionTypeDefaultDescription
namestring(required)Storage key
storageStateStoragelocalStorageStorage adapter
versionstring | number0Schema version for migrations
throttlenumber0Minimum ms between storage writes
skipHydrationbooleanfalseSkip automatic hydration (for async storage)
onDone(data) => void-Called after a successful write
onError(error) => void-Called on read/write failure

Snapshot strategy options

OptionTypeDescription
filter(event) => booleanReturn false to skip persisting for an event
pick(context) => Partial<TContext>Select which parts of context to persist
migrate(persistedContext, version) => TContextMigration function for version upgrades
merge(persisted, current) => TContextCustom merge on rehydration. Defaults to shallow merge
serialize(value) => stringCustom serializer. Defaults to JSON.stringify
deserialize(str) => valueCustom deserializer. Defaults to JSON.parse

Event strategy options

OptionTypeDescription
maxEventsnumberMax events to keep. Excess triggers a checkpoint
migrate(events, version) => eventsMigration function for version upgrades
serialize(value) => stringCustom serializer
deserialize(str) => valueCustom deserializer

Partial persistence

Use pick to persist only specific fields:

.with(persist({
  name: 'my-store',
  pick: (context) => ({ count: context.count }),
}))

Filtering events

Skip storage writes for certain events:

.with(persist({
  name: 'my-store',
  filter: (event) => event.type !== 'hover',
}))

Throttled writes

Debounce storage writes to reduce I/O:

.with(persist({
  name: 'my-store',
  throttle: 1000,
}))

Use flushStorage(store) to force an immediate write (e.g. before page unload):

import { flushStorage } from '@xstate/store/persist'; 

window.addEventListener('beforeunload', () => {
  flushStorage(store);
});

Migrations

Handle schema changes between versions:

.with(persist({
  name: 'my-store',
  version: 2,
  migrate: (persisted, version) => {
    if (version === 1) {
      return { ...persisted, newField: 'default' };
    }
    return persisted;
  },
}))

Async storage

For async storage adapters (React Native AsyncStorage, IndexedDB, etc.), use createJSONStorage with skipHydration and manual rehydration:

import {
  persist,
  rehydrateStore,
  createJSONStorage, 
} from '@xstate/store/persist';

const store = createStore({
  context: { count: 0 },
  on: {
    inc: (context) => ({ count: context.count + 1 }),
  },
}).with(
  persist({
    name: 'my-store',
    storage: createJSONStorage(() => AsyncStorage),
    skipHydration: true,
  }),
);

await rehydrateStore(store);

createJSONStorage is SSR-safe and returns noop storage if the backing store is unavailable.

Cross-tab sync

Sync state across browser tabs using BroadcastChannel:

import {
  persist,
  createBroadcastStorage, 
  subscribeToBroadcastStorage,
  createJSONStorage,
} from '@xstate/store/persist';

const storage = createBroadcastStorage(createJSONStorage(() => localStorage));

const store = createStore({
  context: { count: 0 },
  on: {
    inc: (context) => ({ count: context.count + 1 }),
  },
}).with(persist({ name: 'my-store', storage }));

const unsubscribe = subscribeToBroadcastStorage(store);

When any tab writes to storage, other tabs automatically rehydrate.

Helper functions

FunctionDescription
clearStorage(store)Remove persisted data from storage
flushStorage(store)Force immediate write of pending throttled data
isHydrated(store)Check if the store has been hydrated
rehydrateStore(store)Manually rehydrate from async storage
createJSONStorage(getStorage)SSR-safe storage adapter factory
createBroadcastStorage(storage, options?)Wrap storage for cross-tab sync
subscribeToBroadcastStorage(store)Listen for cross-tab updates

All helper functions are imported from @xstate/store/persist.

On this page