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
| Option | Type | Default | Description |
|---|---|---|---|
name | string | (required) | Storage key |
storage | StateStorage | localStorage | Storage adapter |
version | string | number | 0 | Schema version for migrations |
throttle | number | 0 | Minimum ms between storage writes |
skipHydration | boolean | false | Skip automatic hydration (for async storage) |
onDone | (data) => void | - | Called after a successful write |
onError | (error) => void | - | Called on read/write failure |
Snapshot strategy options
| Option | Type | Description |
|---|---|---|
filter | (event) => boolean | Return false to skip persisting for an event |
pick | (context) => Partial<TContext> | Select which parts of context to persist |
migrate | (persistedContext, version) => TContext | Migration function for version upgrades |
merge | (persisted, current) => TContext | Custom merge on rehydration. Defaults to shallow merge |
serialize | (value) => string | Custom serializer. Defaults to JSON.stringify |
deserialize | (str) => value | Custom deserializer. Defaults to JSON.parse |
Event strategy options
| Option | Type | Description |
|---|---|---|
maxEvents | number | Max events to keep. Excess triggers a checkpoint |
migrate | (events, version) => events | Migration function for version upgrades |
serialize | (value) => string | Custom serializer |
deserialize | (str) => value | Custom 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
| Function | Description |
|---|---|
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.