The devtools middleware integrates your Zustand store with Redux DevTools Extension, enabling time-travel debugging, action inspection, and state snapshots without Redux.
You must install the Redux DevTools Extension in your browser and the package:npm install @redux-devtools/extension
import { devtools } from 'zustand/middleware'
const useStore = create(
devtools(
(set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 }), undefined, 'counter/increment'),
}),
{ name: 'CounterStore' }
)
)
Signature
devtools<T>(
stateCreator: StateCreator<T, [], []>,
options?: DevtoolsOptions
): StateCreator<T, [['zustand/devtools', never]], []>
Parameters
The state creator function that defines your store.
Configuration options for Redux DevTools integration.Custom identifier for the connection in Redux DevTools. Helps distinguish between multiple stores.
enabled
boolean
default:"true in development, false in production"
Enable or disable DevTools integration. Automatically uses NODE_ENV to determine default.
anonymousActionType
string
default:"'anonymous'"
Action type name for mutations without an explicit action name.
Custom identifier for the store instance in DevTools.
Regex patterns to filter actions from DevTools. Useful for hiding internal or sensitive actions.
Basic Usage
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
type Store = {
bears: number
fishes: number
addBear: () => void
addFish: () => void
}
const useJungleStore = create<Store>()(
devtools(
(set) => ({
bears: 0,
fishes: 0,
addBear: () =>
set((state) => ({ bears: state.bears + 1 }), undefined, 'jungle/addBear'),
addFish: () =>
set((state) => ({ fishes: state.fishes + 1 }), undefined, 'jungle/addFish'),
}),
{ name: 'JungleStore' }
)
)
Named Actions
Provide action names as the third parameter to set for better debugging:
const useStore = create(
devtools((set) => ({
count: 0,
increment: () =>
set(
(state) => ({ count: state.count + 1 }),
undefined, // replace flag
'counter/increment' // action name
),
decrement: () =>
set(
(state) => ({ count: state.count - 1 }),
undefined,
'counter/decrement'
),
}))
)
Slices Pattern
When using the slices pattern, properly type your slice creators:
import { create, StateCreator } from 'zustand'
import { devtools } from 'zustand/middleware'
type BearSlice = {
bears: number
addBear: () => void
}
type FishSlice = {
fishes: number
addFish: () => void
}
type JungleStore = BearSlice & FishSlice
const createBearSlice: StateCreator<
JungleStore,
[['zustand/devtools', never]],
[],
BearSlice
> = (set) => ({
bears: 0,
addBear: () =>
set(
(state) => ({ bears: state.bears + 1 }),
undefined,
'jungle:bear/addBear'
),
})
const createFishSlice: StateCreator<
JungleStore,
[['zustand/devtools', never]],
[],
FishSlice
> = (set) => ({
fishes: 0,
addFish: () =>
set(
(state) => ({ fishes: state.fishes + 1 }),
undefined,
'jungle:fish/addFish'
),
})
const useJungleStore = create<JungleStore>()(
devtools((...args) => ({
...createBearSlice(...args),
...createFishSlice(...args),
}))
)
Filtering Actions
Hide sensitive or internal actions from DevTools:
type Store = {
user: string | null
token: string | null
login: (user: string, token: string) => void
logout: () => void
updateInternal: () => void
}
const useStore = create<Store>()(
devtools(
(set) => ({
user: null,
token: null,
login: (user, token) =>
set({ user, token }, undefined, 'auth/login'),
logout: () =>
set({ user: null, token: null }, undefined, 'auth/logout'),
updateInternal: () =>
set({ user: 'updated' }, undefined, 'internal/update'),
}),
{
name: 'AuthStore',
actionsDenylist: ['internal/.*'], // Hide all 'internal/*' actions
}
)
)
// Or filter multiple patterns
const useStore2 = create(
devtools(
(set) => ({ /* ... */ }),
{
name: 'MyStore',
actionsDenylist: ['secret.*', 'internal.*', 'temp.*'],
}
)
)
Cleanup
Clean up DevTools connection when a store is no longer needed:
const useStore = create(
devtools((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}))
)
// When done with the store
useStore.devtools.cleanup()
This is especially useful for dynamically created stores or stores wrapped in context.
Composing with Other Middleware
import { devtools, persist } from 'zustand/middleware'
const useStore = create<Store>()(
devtools(
persist(
(set) => ({
count: 0,
increment: () =>
set((state) => ({ count: state.count + 1 }), undefined, 'increment'),
}),
{ name: 'counter-storage' }
),
{ name: 'CounterStore' }
)
)
Conditional Enabling
const useStore = create(
devtools(
(set) => ({ /* ... */ }),
{
name: 'MyStore',
enabled: process.env.NODE_ENV === 'development',
}
)
)
Troubleshooting
Multiple Stores
Redux DevTools shows one store at a time by default. Use the store selector dropdown to switch between stores.
Anonymous Actions
If actions show as “anonymous”, ensure you’re providing action names:
// Bad - shows as 'anonymous'
set((state) => ({ count: state.count + 1 }))
// Good - shows as 'counter/increment'
set(
(state) => ({ count: state.count + 1 }),
undefined,
'counter/increment'
)
You can customize the anonymous action name:
const useStore = create(
devtools(
(set) => ({ /* ... */ }),
{
name: 'MyStore',
anonymousActionType: 'unknown-action',
}
)
)