# How to Implement Undo/Redo Functionality with Zustand: A Complete Guide

> Learn to implement undo redo functionality in Zustand using zundo middleware. Easily add undo, redo, and clear actions to your store for a better user experience.

- Repository: [Poimandres/zustand](https://github.com/pmndrs/zustand)
- Tags: how-to-guide
- Published: 2026-03-06

---

**Implement undo/redo functionality with Zustand by wrapping your store with the `zundo` middleware, which maintains an internal history stack and injects `undo()`, `redo()`, and `clear()` actions into your store API.**

Zustand's core API intentionally remains minimal to ensure maximum flexibility, providing only a plain-object store, a `setState` updater, and a `subscribe` listener. Because the library avoids baking in advanced features, implementing undo/redo functionality with Zustand requires leveraging the community-maintained **zundo** middleware, which is officially listed in the `pmndrs/zustand` repository's third-party integrations.

## Understanding Zustand's Middleware Architecture

Zustand achieves extensibility through middleware that intercepts the `set` function and store state. The core implementation in [`src/react.ts`](https://github.com/pmndrs/zustand/blob/main/src/react.ts) exports the `create` function that initializes stores, while [`src/middleware.ts`](https://github.com/pmndrs/zustand/blob/main/src/middleware.ts) re-exports built-in utilities like `devtools` and `persist`.

Because undo/redo requires tracking state snapshots across time, it cannot be implemented efficiently through simple state mutations alone. The `zundo` middleware solves this by sitting between your store logic and Zustand's internal `setState`, capturing snapshots before they change and maintaining a history stack independent of your application's state shape.

## How the Zundo Middleware Implements Time-Travel

The `zundo` middleware, referenced in [`docs/reference/integrations/third-party-libraries.md`](https://github.com/pmndrs/zustand/blob/main/docs/reference/integrations/third-party-libraries.md), operates through four core mechanisms:

1. **State History Stack** – Each time your store's `set` function executes, `zundo` pushes a snapshot of the previous state onto an internal history array.
2. **Undo/Redo Pointers** – The middleware tracks a current index within the history stack. When you call `undo()`, it decrements the pointer and rehydrates the store with the snapshot at that position.
3. **Action Filtering** – You can provide a `filter` function to exclude specific actions from history (such as transient UI updates like `SELECT_ITEM`), preventing the stack from filling with irrelevant states.
4. **Memory Management** – The `limit` option caps the history array length, ensuring long-running applications don't consume unbounded memory.

## Basic Implementation: Creating an Undo/Redo Store

To implement undo/redo functionality with Zustand, install `zundo` alongside `zustand`, then wrap your store definition with the middleware:

```bash
npm install zustand zundo

```

Create a basic counter store with time-travel capabilities:

```typescript
import { create } from 'zustand'
import { undo } from 'zundo'

interface CounterState {
  count: number
  inc: () => void
  dec: () => void
}

export const useCounter = create<CounterState>(
  undo((set, get) => ({
    count: 0,
    inc: () => set((state) => ({ count: state.count + 1 })),
    dec: () => set((state) => ({ count: state.count - 1 })),
    
    // These methods are injected by zundo but can be typed explicitly
    undo: () => get().undo?.(),
    redo: () => get().redo?.(),
    clearHistory: () => get().clear?.(),
  }))
)

```

Calling `useCounter.getState().inc()` pushes a new snapshot onto the history stack. Invoking `undo()` restores the previous `count` value without requiring you to manage the history manually.

## Configuring History Limits and Action Filtering

For production applications, you should constrain memory usage and exclude non-essential state changes from the history. The `zundo` middleware accepts an options object as its first argument:

```typescript
import { create } from 'zustand'
import { undo } from 'zundo'

interface TodoState {
  items: string[]
  selectedId: string | null
  add: (item: string) => void
  remove: (index: number) => void
  select: (id: string) => void
}

export const useTodo = create<TodoState>(
  undo(
    {
      limit: 50, // Maintain maximum 50 snapshots
      filter: (action) => {
        // Only track state changes, not UI selections
        return action.type !== 'SELECT_ITEM'
      },
      equality: (current, next) => {
        // Custom equality check to avoid duplicate entries
        return JSON.stringify(current) === JSON.stringify(next)
      }
    },
    (set, get) => ({
      items: [],
      selectedId: null,
      add: (item) => set((state) => ({ 
        items: [...state.items, item] 
      })),
      remove: (i) => set((state) => ({
        items: state.items.filter((_, idx) => idx !== i)
      })),
      select: (id) => set({ selectedId: id }) // Filtered from history
    })
  )
)

```

The `limit` option prevents unbounded memory growth in long-running sessions, while `filter` ensures that transient UI state (like hover effects or selection highlights) doesn't pollute the undo stack.

## Composing Zundo with Other Middleware

Zustand's middleware composition allows `zundo` to work alongside built-in utilities like `devtools` and `persist`. The order of wrapping determines which middleware intercepts state changes first.

In [`src/middleware.ts`](https://github.com/pmndrs/zustand/blob/main/src/middleware.ts), Zustand exports `devtools` and `persist` for common use cases. When combining these with `zundo`, wrap `undo` around your store logic, then apply persistence and devtools:

```typescript
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
import { undo } from 'zundo'

interface CartState {
  items: string[]
  add: (item: string) => void
}

export const useCart = create<CartState>(
  devtools(
    persist(
      undo(
        (set, get) => ({
          items: [],
          add: (item) => set((state) => ({ 
            items: [...state.items, item] 
          })),
          // undo(), redo(), clear() available via zundo
        })
      ),
      {
        name: 'cart-storage',
        getStorage: () => localStorage,
      }
    ),
    { name: 'CartStore' }
  )
)

```

This composition ensures that:
- `undo` captures snapshots of the pure state (before persistence serialization)
- `persist` saves the current state (and history, if configured) to `localStorage`
- `devtools` tracks all actions, including `undo` and `redo` calls, in the Redux DevTools extension

## Using Undo/Redo in React Components

Once the middleware is configured, consuming the undo/redo API in React components requires no special hooks. The actions are part of the store's state object:

```tsx
import { useCounter } from './store/counter'

function Counter() {
  const { count, inc, dec, undo, redo } = useCounter()

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={inc}>+</button>
      <button onClick={dec}>-</button>
      <button onClick={undo}>Undo</button>
      <button onClick={redo}>Redo</button>
    </div>
  )
}

```

The `undo` and `redo` functions are stable references that do not trigger re-renders when called, following Zustand's standard selector optimization patterns.

## Summary

- **Zustand's core** in [`src/react.ts`](https://github.com/pmndrs/zustand/blob/main/src/react.ts) and [`src/middleware.ts`](https://github.com/pmndrs/zustand/blob/main/src/middleware.ts) provides no built-in undo/redo; time-travel requires external middleware.
- **The `zundo` middleware**, listed in [`docs/reference/integrations/third-party-libraries.md`](https://github.com/pmndrs/zustand/blob/main/docs/reference/integrations/third-party-libraries.md), implements undo/redo by maintaining an internal history stack of state snapshots.
- **Configuration options** include `limit` (history size), `filter` (action exclusion), and `equality` (duplicate detection) to optimize memory and performance.
- **Middleware composition** allows `zundo` to wrap cleanly around `persist` and `devtools`, enabling persisted time-travel with full DevTools visibility.
- **React integration** requires no additional hooks; `undo()` and `redo()` are standard actions available on the store object.

## Frequently Asked Questions

### Can I implement undo/redo without external middleware?

While technically possible by manually managing a history array within your store's state, Zustand's minimal core in [`src/react.ts`](https://github.com/pmndrs/zustand/blob/main/src/react.ts) does not provide history management primitives. Implementing time-travel manually requires intercepting every `set` call, deep-cloning state snapshots, and managing pointer indices—functionality that the `zundo` middleware already optimizes. For production applications, using the community-maintained middleware is the recommended approach.

### How does zundo handle memory management?

The `zundo` middleware provides a `limit` configuration option that caps the number of state snapshots stored in the history array. When the limit is reached, the oldest snapshots are discarded from the beginning of the stack while maintaining the current pointer position. Additionally, the `filter` option allows you to exclude transient state changes (like UI hover states) from being recorded, further reducing memory overhead in long-running applications.

### Does zundo work with Zustand's persist middleware?

Yes, `zundo` composes cleanly with the `persist` middleware exported from [`src/middleware.ts`](https://github.com/pmndrs/zustand/blob/main/src/middleware.ts). When wrapping your store, apply `undo` inside `persist` if you want the history stack itself saved to storage, or outside `persist` if you only want the current state persisted while keeping history in memory. The middleware order determines whether time-travel state survives page reloads, allowing flexible persistence strategies based on your application's requirements.

### Can I use zundo with TypeScript?

The `zundo` middleware is fully typed and supports TypeScript stores. When defining your store interface, include the undo/redo actions in your type definition or use the utility types provided by the package. The middleware preserves type safety for your state slices while injecting the `undo()`, `redo()`, and `clear()` methods into the store API, ensuring full IntelliSense support in React components and preventing type errors during history navigation.