How to Integrate Zustand with Redux DevTools for Debugging: A Complete Guide

Zustand integrates with Redux DevTools through the devtools middleware, which intercepts state changes in src/middleware/devtools.ts and forwards them to the browser extension while supporting time-travel debugging and multiple store tracking.

The pmndrs/zustand repository provides a lightweight yet powerful devtools middleware that connects Zustand stores directly to the Redux DevTools Extension without requiring Redux itself. This integration allows you to inspect state changes, travel back in time, and dispatch actions from the browser extension while maintaining Zustand's minimal API surface.

How the Devtools Middleware Works

The middleware implementation in src/middleware/devtools.ts wraps your store's setState method to create a bridge between Zustand and the Redux DevTools Extension. Understanding this mechanism helps you debug effectively and configure advanced options.

Extension Detection and Initialization

At store creation, the middleware checks for the presence of window.__REDUX_DEVTOOLS_EXTENSION__. If the extension is missing or running in production (without explicit override), the middleware returns the original StateCreator unchanged, ensuring zero overhead when debugging is disabled.

const extensionConnector = (enabled ?? import.meta.env?.MODE !== 'production')
  && window.__REDUX_DEVTOOLS_EXTENSION__;

This detection occurs at lines 98-104 of devtools.ts, providing automatic environment-aware behavior.

Connection Handling and Store Grouping

The middleware manages connections through two patterns:

  • Single-store mode: When the store option is undefined, the middleware creates a dedicated DevTools connection for that store.
  • Multi-store mode: When you provide a store identifier, the middleware maintains a global map called trackedConnections that groups multiple stores under one DevTools connection. This allows you to debug several stores simultaneously using the extension's selector.

The logic resides in extractConnectionInformation and the trackedConnections management system (lines 38-61).

State Mutation Interception

The core functionality replaces the original set function with a wrapper that:

  1. Executes the state update
  2. Builds an action descriptor ({type: ...}) using the explicit action name (third argument to set), the anonymousActionType option, or a heuristic stack trace analysis via findCallerName
  3. Sends the action and new state to the extension via connection?.send
  4. For store-scoped connections, prefixes the action type with ${store}/${action.type} and sends a snapshot of all tracked stores

This interception logic spans lines 155-191 of devtools.ts.

Inbound Message Processing

The middleware subscribes to DevTools messages and handles various dispatch types through a switch block starting at line 300:

  • ACTION: Parses payload data and applies __setState actions to replace store state
  • DISPATCH: Processes RESET, COMMIT, ROLLBACK, JUMP_TO_STATE, and IMPORT_STATE operations using the setStateFromDevtools helper to restore previous states

Setting Up Redux DevTools in Zustand

Installation Requirements

Install the DevTools extension for your browser, then ensure your environment supports the connection. No additional npm packages are strictly required for the basic integration, though TypeScript users may reference @redux-devtools/extension for types.

Basic Store Configuration

Wrap your store creator with the devtools middleware imported from zustand/middleware:

import { create } from 'zustand';
import { devtools } from 'zustand/middleware';

type Counter = {
  count: number;
  inc: () => void;
};

export const useCounter = create<Counter>()(
  devtools(
    (set) => ({
      count: 0,
      inc: () => set((s) => ({ count: s.count + 1 }), undefined, 'counter/inc'),
    }),
    {
      name: 'CounterStore',
      enabled: true,
      anonymousActionType: 'unknown_action',
    },
  ),
);

The third argument to set ('counter/inc' in the example) defines the action name visible in the DevTools timeline.

Naming Actions for Clear Debugging

Always provide explicit action names as the third parameter to setState to avoid anonymous actions in your DevTools history:

// Good: Clear action name
set({ user: newUser }, undefined, 'auth/setUser');

// Avoid: Relies on anonymousActionType fallback or stack heuristics
set({ user: newUser });

Multiple Stores and Scoped Connections

When managing several stores, give each a unique store identifier to group them under a single DevTools connection:

export const useAuth = create<Auth>()(
  devtools(authCreator, { store: 'auth' })
);

export const useProfile = create<Profile>()(
  devtools(profileCreator, { store: 'profile' })
);

This configuration enables switching between stores via the DevTools instance selector while maintaining isolated state management.

Cleanup on Unmount

For dynamically created stores (such as those inside React contexts), unsubscribe from DevTools when the component unmounts to prevent memory leaks and orphaned connections:

useEffect(() => {
  return () => {
    useMyStore.devtools.cleanup();
  };
}, []);

The cleanup method (lines 246-256 in devtools.ts) removes the store from trackedConnections and closes the DevTools subscription.

Advanced Configuration Options

The devtools middleware accepts an options object that forwards settings to the Redux DevTools Extension:

Option Description
name Display name for the connection in the DevTools UI
enabled Boolean override; defaults to true in development, false in production
anonymousActionType Fallback label when actions omit explicit names
store Unique key for scoping state in multi-store setups
actionsDenylist Regex or array of regexes to hide internal actions from the timeline

Example with action filtering:

devtools(storeCreator, {
  name: 'AuthStore',
  actionsDenylist: ['internal/.*', 'secret/.*'],
})

TypeScript Implementation with Slices

When using the slices pattern, type each slice with the [['zustand/devtools', never]] mutator to preserve type safety through the middleware chain:

import { create, StateCreator } from 'zustand';
import { devtools } from 'zustand/middleware';

type BearSlice = { bears: number; addBear: () => void };
type FishSlice = { fish: number; addFish: () => void };
type JungleStore = BearSlice & FishSlice;

const bearSlice: StateCreator<JungleStore, [['zustand/devtools', never]], [], BearSlice> = (set) => ({
  bears: 0,
  addBear: () => set(s => ({ bears: s.bears + 1 }), undefined, 'bear/add'),
});

const fishSlice: StateCreator<JungleStore, [['zustand/devtools', never]], [], FishSlice> = (set) => ({
  fish: 0,
  addFish: () => set(s => ({ fish: s.fish + 1 }), undefined, 'fish/add'),
});

export const useJungle = create<JungleStore>()(
  devtools((...args) => ({
    ...bearSlice(...args),
    ...fishSlice(...args),
  }), { name: 'Jungle' }),
);

According to the pmndrs/zustand documentation, devtools should always be the outermost middleware in the composition chain to properly intercept all state changes (see docs/learn/guides/advanced-typescript.md).

Summary

  • The devtools middleware in src/middleware/devtools.ts provides Redux DevTools integration by wrapping setState and forwarding changes to the browser extension.
  • It automatically detects the Redux DevTools Extension via window.__REDUX_DEVTOOLS_EXTENSION__ and gracefully degrades when unavailable.
  • Use the third argument of set to provide descriptive action names for the DevTools timeline.
  • Configure the store option to group multiple Zustand stores under a single DevTools connection using the internal trackedConnections map.
  • Call store.devtools.cleanup() when unmounting dynamically created stores to prevent memory leaks.
  • Apply the [['zustand/devtools', never]] mutator type to slices when using TypeScript to maintain type safety through the middleware chain.

Frequently Asked Questions

How do I hide internal actions from Redux DevTools?

Use the actionsDenylist option in the devtools configuration, which accepts regex patterns that match action types you want to exclude from the timeline. For example, actionsDenylist: ['internal/.*'] hides all actions prefixed with "internal/" while keeping public actions visible.

Can I use Redux DevTools with Zustand without installing Redux?

Yes. Zustand's devtools middleware communicates directly with the Redux DevTools Extension browser plugin without requiring the Redux library as a dependency. The middleware implements the extension's communication protocol independently, as defined in src/middleware/devtools.ts.

Why are my state changes not appearing in Redux DevTools?

First, verify that enabled is not explicitly set to false and that you are not in a production build (unless enabled is explicitly true). Ensure you are passing an action name as the third argument to set, or check that the extension is installed and window.__REDUX_DEVTOOLS_EXTENSION__ is available in your browser console. For TypeScript users, ensure devtools is the outermost middleware wrapper.

How do I debug multiple Zustand stores simultaneously?

Assign each store a unique store identifier in the devtools options. The middleware maintains a global trackedConnections map that groups these stores under a single DevTools connection, allowing you to switch between store states using the extension's dropdown selector while preserving independent state management logic.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →