enable time-travel in your apps. undo/redo middleware for zustand. built with zustand. <700 B

Try a live demo
npm i zustand zundo
zustand v4.2.0+ or v5 is required for TS usage. v4.0.0 or higher is required for JS usage. Node 16 or higher is required.

temporal middlewareThis returns the familiar store accessible by a hook! But now your store also tracks past states.
import { create } from 'zustand';
import { temporal } from 'zundo';
// Define the type of your store state (typescript)
interface StoreState {
bears: number;
increasePopulation: () => void;
removeAllBears: () => void;
}
// Use `temporal` middleware to create a store with undo/redo capabilities
const useStoreWithUndo = create<StoreState>()(
temporal((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
})),
);
temporal functions and properties of your storeYour zustand store will now have an attached temporal object that provides access to useful time-travel utilities, including undo, redo, and clear!
const App = () => {
const { bears, increasePopulation, removeAllBears } = useStoreWithUndo();
// See API section for temporal.getState() for all functions and
// properties provided by `temporal`, but note that properties, such as `pastStates` and `futureStates`, are not reactive when accessed directly from the store.
const { undo, redo, clear } = useStoreWithUndo.temporal.getState();
return (
<>
bears: {bears}
<button onClick={() => increasePopulation}>increase</button>
<button onClick={() => removeAllBears}>remove</button>
<button onClick={() => undo()}>undo</button>
<button onClick={() => redo()}>redo</button>
<button onClick={() => clear()}>clear</button>
</>
);
};
temporal object, optionally convert to a React store hookIn React, to subscribe components or custom hooks to member properties of the temporal object (like the array of pastStates or currentStates), you can create a useTemporalStore hook.
import { useStoreWithEqualityFn } from 'zustand/traditional';
import type { TemporalState } from 'zundo';
function useTemporalStore(): TemporalState<MyState>;
function useTemporalStore<T>(selector: (state: TemporalState<MyState>) => T): T;
function useTemporalStore<T>(
selector: (state: TemporalState<MyState>) => T,
equality: (a: T, b: T) => boolean,
): T;
function useTemporalStore<T>(
selector?: (state: TemporalState<MyState>) => T,
equality?: (a: T, b: T) => boolean,
) {
return useStoreWithEqualityFn(useStoreWithUndo.temporal, selector!, equality);
}
const App = () => {
const { bears, increasePopulation, removeAllBears } = useStoreWithUndo();
// changes to pastStates and futureStates will now trigger a reactive component rerender
const { undo, redo, clear, pastStates, futureStates } = useTemporalStore(
(state) => state,
);
return (
<>
bears: {bears}
pastStates: {JSON.stringify(pastStates)}
futureStates: {JSON.stringify(futureStates)}
<button onClick={() => increasePopulation}>increase</button>
<button onClick={() => removeAllBears}>remove</button>
<button onClick={() => undo()}>undo</button>
<button onClick={() => redo()}>redo</button>
<button onClick={() => clear()}>clear</button>
</>
);
};
(config: StateCreator, options?: ZundoOptions) => StateCreator
zundo has one export: temporal. It is used as middleware for create from zustand. The config parameter is your store created by zustand. The second options param is optional and has the following API.
export interface ZundoOptions<TState, PartialTState = TState> {
partialize?: (state: TState) => PartialTState;
limit?: number;
equality?: (pastState: PartialTState, currentState: PartialTState) => boolean;
diff?: (
pastState: Partial<PartialTState>,
currentState: Partial<PartialTState>,
) => Partial<PartialTState> | null;
onSave?: (pastState: TState, currentState: TState) => void;
handleSet?: (
handleSet: StoreApi<TState>['setState'],
) => StoreApi<TState>['setState'];
pastStates?: Partial<PartialTState>[];
futureStates?: Partial<PartialTState>[];
wrapTemporal?: (
storeInitializer: StateCreator<
_TemporalState<TState>,
[StoreMutatorIdentifier, unknown][],
[]
>,
) => StateCreator<
_TemporalState<TState>,
[StoreMutatorIdentifier, unknown][],
[StoreMutatorIdentifier, unknown][]
>;
}
partialize?: (state: TState) => PartialTState
Use the partialize option to omit or include specific fields. Pass a callback that returns the desired fields. This can also be used to exclude fields. By default, the entire state object is tracked.
// Only field1 and field2 will be tracked
const useStoreWithUndoA = create<StoreState>()(
temporal(
(set) => ({
// your store fields
}),
{
partialize: (state) => {
const { field1, field2, ...rest } = state;
return { field1, field2 };
},
},
),
);
// Everything besides field1 and field2 will be tracked
const useStoreWithUndoB = create<StoreState>()(
temporal(
(set) => ({
// your store fields
}),
{
partialize: (state) => {
const { field1, field2, ...rest } = state;
return rest;
},
},
),
);
useTemporalStore with partializeIf converting temporal store to a React Store Hook with typescript, be sure to define the type of your partialized state
interface StoreState {
bears: number;
untrackedStateField: number;
}
type PartializedStoreState = Pick<StoreState, 'bears'>;
const useStoreWithUndo = create<StoreState>()(
temporal(
(set) => ({
bears: 0,
untrackedStateField: 0,
}),
{
partialize: (state) => {
const { bears } = state;
return { bears };
},
},
),
);
const useTemporalStore = <T,>(
// Use partalized StoreState type as the generic here
selector: (state: TemporalState<PartializedStoreState>) => T,
) => useStore(useStoreWithUndo.temporal, selector);
limit?: number
For performance reasons, you may want to limit the number of previous and future states stored in history. Setting limit will limit the number of previous and future states stored in the temporal store. When the limit is reached, the oldest state is dropped. By default, no limit is set.
const useStoreWithUndo = create<StoreState>()(
temporal(
(set) => ({
// your store fields
}),
{ limit: 100 },
),
);
equality?: (pastState: PartialTState, currentState: PartialTState) => boolean
By default, a state snapshot is stored in temporal history when any zustand state setter is called—even if no value in your zustand store has changed.
If all of your zustand state setters modify state in a way that you want tracked in history, this default is sufficient.
However, for more precise control over when a state snapshot is stored in zundo history, you can provide an equality function.
You can write your own equality function or use something like fast-equals, fast-deep-equal, zustand/shallow, lodash.isequal, or underscore.isEqual.
import isDeepEqual from 'fast-deep-equal';
// Use a deep equality function to only store history when currentState has changed
const useStoreWithUndo = create<StoreState>()(
temporal(
(set) => ({
// your store fields
}),
// a state snapshot will only be stored in history when currentState is not deep-equal to pastState
// Note: this can also be more concisely written as {equality: isDeepEqual}
{
equality: (pastState, currentState) =>
isDeepEqual(pastState, currentState),
},
),
);
If your state or specific application does not require deep equality (for example, if you're only using non-nested primitives), you may for performance reasons choose to use a shallow equality fn that does not do deep comparison.
import shallow from 'zustand/shallow';
const useStoreWithUndo = create<StoreState>()(
temporal(
(set) => ({
// your store fields
}),
// a state snapshot will only be stored in history when currentState is not deep-equal to pastState
// Note: this can also be more concisely written as {equality: shallow}
{
equality: (pastState, currentState) => shallow(pastState, currentState),
},
),
);
You can also just as easily use custom equality functions for your specific application
const useStoreWithUndo = create<StoreState>()(
temporal(
(set) => ({
// your store fields
}),
{
// Only track history when field1 AND field2 diverge from their pastState
// Why would you do this? I don't know! But you can do it!
equality: (pastState, currentState) =>
pastState.field1 !== currentState.field1 &&
pastState.field2 !== currentState.field2,
},
),
);
diff?: (pastState: Partial<PartialTState>, currentState: Partial<PartialTState>) => Partial<PartialTState> | null
For performance reasons, you may want to store the state delta rather than the complete (potentially partialized) state object. This can be done by passing a diff function. The diff function should return an object that represents the difference between the past and current state. By default, the full state object is stored.
If diff returns null, the state change will not be tracked. This is helpful for a conditionally storing past states or if you have a doNothing action that does not change the state.
You can write your own or use something like microdiff, just-diff, or deep-object-diff.
const useStoreWithUndo = create<StoreState>()(
temporal(
(set) => ({
// your store fields
}),
{
diff: (pastState, currentState) => {
const myDiff = diff(currentState, pastState);
const newStateFromDiff = myDiff.reduce(
(acc, difference) => {
type Key = keyof typeof currentState;
if (difference.type === 'CHANGE') {
const pathAsString = difference.path.join('.') as Key;
acc[pathAsString] = difference.value;
}
return acc;
},
{} as Partial<typeof currentState>,
);
return isEmpty(newStateFromDiff) ? null : newStateFromDiff;
},
},
),
);
onSave?: (pastState: TState, currentState: TState) => void
Sometimes, you may need to call a function when the temporal store is updated. This can be configured using onSave in the options, or by programmatically setting the callback if you need lexical context (see the TemporalState API below for more information).
import { shallow } from 'zustand/shallow';
const useStoreWithUndo = create<StoreState>()(
temporal(
(set) => ({
// your store fields
}),
{ onSave: (state) => console.log('saved', state) },
),
);
handleSet?: (handleSet: StoreApi<TState>['setState']) => (
pastState: Parameters<StoreApi<TState>['setState']>[0],
// `replace` will likely be deprecated and removed in the future
replace: Parameters<StoreApi<TState>['setState']>[1],
currentState: PartialTState,
deltaState?: Partial<PartialTState> | null,
) => void
Sometimes multiple state changes might happen in a short amount of time and you only want to store one change in history. To do so, we can utilize the handleSet callback to set a timeout to prevent new changes from being stored in history. This can be used with something like [throttle-debounce](https://github.com/nik
$ claude mcp add zundo \
-- python -m otcore.mcp_server <graph>