Stately

Undo/redo

The undoRedo extension adds undo and redo triggers to a store with two strategy options.

import { createStore } from '@xstate/store';
import { undoRedo } from '@xstate/store/undo'; 

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

store.trigger.inc(); // count = 1
store.trigger.inc(); // count = 2
store.trigger.undo(); // count = 1
store.trigger.redo(); // count = 2

Strategies

Event strategy (default)

Stores the full event log. On undo, events are popped from the log and the store is rebuilt by replaying remaining events from the initial state. Best for deterministic transitions.

.with(undoRedo()) // or
.with(undoRedo({ strategy: 'event' }))

Snapshot strategy

Stores past/future snapshots directly. Faster undo/redo since no replay is needed, but uses more memory.

.with(undoRedo({
  strategy: 'snapshot', 
  historyLimit: 50,
}))
OptionTypeDefaultStrategyDescription
strategy'event' | 'snapshot''event'bothHistory storage strategy
getTransactionId(event, snapshot) => string | null-bothGroup related events into a single undo step
skipEvent(event, snapshot) => boolean-bothExclude events from history
historyLimitnumberInfinitysnapshotMax snapshots to keep
compare(past, current) => boolean-snapshotSkip duplicate snapshots

Transactions

Group multiple events into a single undo step by returning the same transaction ID:

const store = createStore({
  context: { items: [] as string[] },
  on: {
    addItem: (context, event: { item: string }) => ({
      items: [...context.items, event.item],
    }),
  },
}).with(
  undoRedo({
    getTransactionId: (event) => event.batchId ?? null,
  }),
);

store.trigger.addItem({ item: 'a', batchId: 'batch-1' });
store.trigger.addItem({ item: 'b', batchId: 'batch-1' });
store.trigger.addItem({ item: 'c', batchId: 'batch-1' });

// All three are undone in one step
store.trigger.undo(); // items = []

Skipping events

Exclude certain events from history so they cannot be undone:

const store = createStore({
  context: { count: 0, theme: 'light' },
  on: {
    inc: (context) => ({ ...context, count: context.count + 1 }),
    setTheme: (context, event: { theme: string }) => ({
      ...context,
      theme: event.theme,
    }),
  },
}).with(
  undoRedo({
    skipEvent: (event) => event.type === 'setTheme',
  }),
);

Deduplication (snapshot strategy)

Avoid storing duplicate snapshots when the state didn't actually change:

.with(undoRedo({
  strategy: 'snapshot',
  compare: (past, current) => past.context.count === current.context.count,
}))

On this page