# How to Debug and Log Zustand State Changes in Development

> Debug Zustand state changes with Redux DevTools integration or manual subscription logging. Track every mutation effortlessly in development for better error resolution.

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

---

**Zustand provides built-in debugging through Redux DevTools integration and manual subscription logging, allowing you to track every state mutation in development.**

The `pmndrs/zustand` library ships with minimal yet powerful debugging capabilities that leverage its publish-subscribe architecture. Because the core store implementation exposes direct subscription methods and middleware hooks, you can inspect state changes using browser DevTools extensions or simple console logging. This guide covers the exact implementation details found in the source code to help you set up effective debugging workflows.

## Understanding the Core Store Architecture

Zustand’s state management revolves around a tiny publish-subscribe system implemented in **[`src/vanilla.ts`](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts)**. The `createStore` function initializes a store with internal `setState` and `subscribe` methods that form the foundation of all debugging capabilities.

When `setState` is called, it mutates the internal state object and immediately notifies every registered listener via `listeners.forEach` (lines 66-80). The `subscribe` method (lines 88-92) simply registers a callback to this listener set and returns an unsubscribe function. This design means **every state change is observable** by attaching a listener to the store instance.

## Debugging with Redux DevTools

The `devtools` middleware in **[`src/middleware/devtools.ts`](https://github.com/pmndrs/zustand/blob/main/src/middleware/devtools.ts)** provides first-class integration with the Redux DevTools browser extension. It wraps the store’s `setState` method to intercept every mutation and forward it as a labeled action to the DevTools UI.

### Configuring the DevTools Middleware

The middleware establishes a connection to the Redux DevTools extension and replaces the native `setState` with a wrapper that builds action descriptions. According to the source code (lines 16-33 and 90-106), the wrapper constructs an action object with a `type` property and sends it via `connection?.send(action, get())`.

```typescript
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'

const useStore = create(
  devtools(
    (set) => ({
      count: 0,
      inc: () => set((s) => ({ count: s.count + 1 }), undefined, 'inc')
    }),
    {
      name: 'CounterStore',
      anonymousActionType: 'unknown',
      enabled: true
    }
  )
)

```

The `enabled` flag defaults to `true` in non-production builds (lines 98-103), automatically activating debugging only during development.

### Naming Actions for Clear Logs

To make DevTools entries meaningful, pass a string as the third argument to `set()`:

```typescript
set((state) => ({ count: state.count + 1 }), undefined, 'counter/increment')

```

If you omit the name, the middleware falls back to `anonymousActionType` or attempts to infer the caller name from the stack trace. You can also pass a full action object for richer logging:

```typescript
set(
  (state) => ({ count: state.count + 1 }),
  undefined,
  { type: 'counter/inc', payload: 1 }
)

```

## Logging State Changes to the Console

For quick debugging without browser extensions, attach listeners directly to the store using the native `subscribe` method.

### Basic Subscription Logging

The `subscribe` method receives the new state (and optionally the previous state) whenever `setState` executes:

```typescript
const useStore = create((set) => ({
  count: 0,
  inc: () => set((s) => ({ count: s.count + 1 }))
}))

// Log every full state change
useStore.subscribe((newState) => console.log('state →', newState))

```

Because listeners receive the complete state object, you can implement custom diffing logic to highlight specific changes between updates.

### Selective Logging with subscribeWithSelector

When you only need to watch a specific property, importing `subscribeWithSelector` from **[`src/middleware/subscribeWithSelector.ts`](https://github.com/pmndrs/zustand/blob/main/src/middleware/subscribeWithSelector.ts)** provides an optimized solution. This middleware wraps the original `api.subscribe` to accept a selector function and an optional equality comparator.

```typescript
import { create } from 'zustand'
import { subscribeWithSelector } from 'zustand/middleware'

const useStore = create(
  subscribeWithSelector((set) => ({
    count: 0,
    text: '',
    inc: () => set((s) => ({ count: s.count + 1 }))
  }))
)

// Log only when `count` changes
useStore.subscribe(
  (state) => state.count,
  (newCount, oldCount) => console.log('count changed:', oldCount, '→', newCount)
)

```

The selector is evaluated on every state update (lines 54-62), and the listener fires only when `equalityFn` (defaulting to `Object.is`) detects a difference. This prevents noisy logs when unrelated properties mutate.

## Creating a Custom Logger Middleware

For reusable debugging logic that works without DevTools, compose a custom middleware that wraps the `set` function:

```typescript
import { create, StateCreator } from 'zustand'

const logger = <T>(config: StateCreator<T>) =>
  (set, get, api) => {
    const loggingSet: typeof set = (partial, replace, name) => {
      console.log('[log] action:', name ?? 'anonymous', 'Δ', partial)
      set(partial, replace, name)
    }
    return config(loggingSet, get, api)
  }

const useStore = create(
  logger((set) => ({
    count: 0,
    inc: () => set((s) => ({ count: s.count + 1 }), undefined, 'inc')
  }))
)

```

This pattern intercepts every `set` call to print the action name and partial state update before delegating to the original setter.

## Combining DevTools and Console Logging

A robust development setup typically combines both approaches: DevTools for time-travel debugging and console subscriptions for ad-hoc monitoring.

```typescript
import { create } from 'zustand'
import { devtools, subscribeWithSelector } from 'zustand/middleware'

const useStore = create(
  subscribeWithSelector(
    devtools(
      (set) => ({
        count: 0,
        user: null,
        inc: () => set((s) => ({ count: s.count + 1 }), undefined, 'inc')
      }),
      { name: 'AppStore', anonymousActionType: '⚡' }
    )
  )
)

// Console-only monitoring for specific slices
useStore.subscribe(
  (s) => s.count,
  (c) => console.log('[dev] count updated:', c)
)

```

All state changes appear in the Redux DevTools timeline with full action descriptions, while the console subscription prints concise updates only when `count` actually changes.

## Summary

- **Core architecture**: The store in [`src/vanilla.ts`](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts) notifies all listeners synchronously after every `setState` call, making all changes observable.
- **Redux DevTools**: The `devtools` middleware in [`src/middleware/devtools.ts`](https://github.com/pmndrs/zustand/blob/main/src/middleware/devtools.ts) wraps `setState` to send labeled actions to the browser extension, supporting custom action names via the third `set` parameter.
- **Console logging**: Direct `store.subscribe()` calls provide immediate state inspection without external dependencies.
- **Selective logging**: `subscribeWithSelector` (from [`src/middleware/subscribeWithSelector.ts`](https://github.com/pmndrs/zustand/blob/main/src/middleware/subscribeWithSelector.ts)) filters updates using selector functions and `Object.is` equality checks.
- **Custom middleware**: Wrapping the `set` function creates reusable loggers that capture action names and partial updates.

## Frequently Asked Questions

### How do I enable Redux DevTools in a Zustand store?

Import the `devtools` middleware from `zustand/middleware` and wrap your store creator. Pass a configuration object with `name` to identify your store in the DevTools panel and set `enabled: true` to ensure it activates in development. Every call to `set()` will then appear as a traceable action in the Redux DevTools extension.

### Can I log only specific properties instead of the entire state?

Yes, apply the `subscribeWithSelector` middleware to your store, then call `subscribe` with two arguments: a selector function that extracts the property you want to watch, and a listener that receives the new and previous values. The listener fires only when the selected slice changes according to the default `Object.is` comparison.

### What is the difference between `subscribe` and `subscribeWithSelector`?

The base `subscribe` method (from [`src/vanilla.ts`](https://github.com/pmndrs/zustand/blob/main/src/vanilla.ts)) invokes its listener on every state change with the full state object. `subscribeWithSelector` (from [`src/middleware/subscribeWithSelector.ts`](https://github.com/pmndrs/zustand/blob/main/src/middleware/subscribeWithSelector.ts)) extends this to accept a selector and equality function, only calling your listener when the selected value actually differs, which improves performance and reduces log noise.

### How can I name my actions for better DevTools debugging?

Pass a string as the third argument to the `set` function: `set(updater, false, 'myActionName')`. If you use the `devtools` middleware, this string becomes the action type in the DevTools UI. Without an explicit name, the middleware falls back to the `anonymousActionType` configuration option or marks the action as "anonymous".