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 = 2Strategies
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,
}))| Option | Type | Default | Strategy | Description |
|---|---|---|---|---|
strategy | 'event' | 'snapshot' | 'event' | both | History storage strategy |
getTransactionId | (event, snapshot) => string | null | - | both | Group related events into a single undo step |
skipEvent | (event, snapshot) => boolean | - | both | Exclude events from history |
historyLimit | number | Infinity | snapshot | Max snapshots to keep |
compare | (past, current) => boolean | - | snapshot | Skip 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,
}))