Zundo: Undo/Redo Middleware for Zustand
Zundo is a lightweight (under 700 bytes) undo/redo middleware designed for Zustand, enabling robust time-travel capabilities in JavaScript and TypeScript applications. Currently at version 2.3.0, it is actively maintained and frequently updated to ensure compatibility with new Zustand versions, including both v4 and v5. Zundo differentiates itself through its minimal bundle size, high flexibility offered by various optional middleware configurations for performance optimization, and an unopinionated, extensible API. It integrates seamlessly with existing Zustand projects, working effectively with multiple stores within a single application to provide undo/redo functionality without significant overhead. Zundo facilitates managing complex state changes by providing simple yet powerful history management.
Common errors
-
TypeError: Cannot read properties of undefined (reading 'temporal')
cause Attempting to access `temporal` functions (e.g., `useStore.temporal.getState()`) before the middleware has been correctly applied or if the store itself is not initialized.fixEnsure `temporal` middleware is correctly wrapped around your `create` function: `create<StoreState>()(temporal((set) => ({...})))`. Also, verify that the store is imported and available where `temporal` is being accessed. -
ModuleNotFoundError: Package path ./dist/esm/index.js is not exported from package ... (see exports field in .../node_modules/zundo/package.json)
cause This error specifically occurs if using zundo v2.0.1 in a CommonJS environment, as it temporarily broke CJS support by changing the default module output to ESM.fixUpgrade zundo to v2.0.2 or higher. Version 2.0.2 restored proper dual CJS/ESM support. -
Argument of type '(set: StoreSet<StoreState>) => { ... }' is not assignable to parameter of type 'StoreInitializer<StoreState>'cause TypeScript error related to `setState` arguments when using zundo with Zustand v5, often due to stricter `setState` types.fixAdjust your `set` calls within the store to conform to Zustand v5's stricter `setState` types, especially regarding the `replace` flag. Refer to Zustand's migration guide for v5 type changes.
Warnings
- breaking Version 2.0.0 was a complete rewrite, introducing significant API changes and a new architecture. Users migrating from v1 will need to update their store definitions and temporal API calls.
- breaking Version 2.0.1 temporarily changed the default module output from CommonJS (CJS) to ESM, which could break CJS-only environments. This was quickly rectified in v2.0.2.
- breaking In v2.1.0, a bug fix for complex cases of `zundo` and an update to `handleSet` arguments (including `currentState` and `deltaState`) changed previously buggy behavior. If your application relied on the prior buggy behavior, this constitutes a breaking change.
- gotcha When accessing properties like `pastStates` or `futureStates` directly from `useStoreWithUndo.temporal.getState()`, these properties are not reactive in React components. Changes will not trigger re-renders.
- gotcha With Zustand v5, the `setState` type became stricter. While zundo v2.3.0 supports these stricter types, applications upgrading to Zustand v5 might encounter TypeScript errors in their `set` calls if they previously used less strict types.
Install
-
npm install zundo -
yarn add zundo -
pnpm add zundo
Imports
- temporal
import temporal from 'zundo'; // Not a default export
import { temporal } from 'zundo'; - TemporalState
import type { TemporalState } from 'zundo'; - CommonJS require
const temporal = require('zundo'); // Requires destructuringconst { temporal } = require('zundo');
Quickstart
import { create } from 'zustand';
import { temporal } from 'zundo';
import { useStoreWithEqualityFn } from 'zustand/traditional';
import type { TemporalState } from 'zundo';
interface StoreState {
bears: number;
increasePopulation: () => void;
removeAllBears: () => void;
}
const useStoreWithUndo = create<StoreState>()(
temporal((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
})),
);
// To access temporal functions and reactive temporal state
interface MyTemporalState extends StoreState {
temporal: TemporalState<StoreState>;
}
const useTemporalStore = () => useStoreWithEqualityFn(useStoreWithUndo.temporal.getState);
const App = () => {
const { bears, increasePopulation, removeAllBears } = useStoreWithUndo();
const { undo, redo, clear, pastStates, futureStates } = useTemporalStore();
return (
<>
<h1>Bears: {bears}</h1>
<button onClick={increasePopulation}>Increase</button>
<button onClick={removeAllBears}>Remove All</button>
<button onClick={() => undo()} disabled={pastStates.length === 0}>Undo</button>
<button onClick={() => redo()} disabled={futureStates.length === 0}>Redo</button>
<button onClick={() => clear()}>Clear History</button>
<p>Past states count: {pastStates.length}</p>
<p>Future states count: {futureStates.length}</p>
</>
);
};
export default App;