What is middleware in Zustand?
In Zustand, the term middleware means a wrapper function that extends the behavior of the store by adding extra logic between a state change (set) and its actual application.
In simpler terms:
Middleware in Zustand is plugins that "hook into" the store and add new capabilities: persistence, logging, devtools, immutability, and so on.
How it works
When you create a store:
import { create } from 'zustand'
const useStore = create((set) => ({
count: 0,
inc: () => set((s) => ({ count: s.count + 1 })),
}))Zustand lets you wrap this "plain" store with a middleware function to change or extend its behavior:
import { devtools } from 'zustand/middleware'
const useStore = create(
devtools((set) => ({
count: 0,
inc: () => set((s) => ({ count: s.count + 1 })),
}))
)Now Zustand connects to Redux DevTools, and you can track state in the browser.
Popular middleware in Zustand
| Middleware | Purpose |
|---|---|
devtools | integration with Redux DevTools for debugging |
persist | saves state to localStorage, sessionStorage, or IndexedDB |
immer | lets you change state "imperatively" through a draft (like in Redux Toolkit) |
subscribeWithSelector | subscription to only the fields you need (deep selectivity) |
combine | helps combine several state slices into one store |
redux | emulates Redux-like behavior with actions and reducers |
Usage examples
1. persist
Persists state across page reloads:
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
const useAuthStore = create(
persist(
(set) => ({
user: null,
setUser: (user) => set({ user }),
logout: () => set({ user: null }),
}),
{ name: 'auth-storage' } // the key in localStorage
)
)Now the data will be automatically stored in localStorage.
2. devtools
Adds integration with Redux DevTools:
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
const useCounterStore = create(
devtools((set) => ({
count: 0,
inc: () => set((s) => ({ count: s.count + 1 })),
}))
)Now you can open DevTools and see every state change.
3. immer
Lets you use a mutable-style update (through a draft):
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'
const useTodoStore = create(
immer((set) => ({
todos: [],
addTodo: (text) =>
set((state) => {
state.todos.push({ text, done: false })
}),
}))
)Here you can "mutate" the array directly, as if it were a plain JS object - under the hood Zustand does an immutable copy.
4. Combining middleware
You can use several middleware at once - just nest them:
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
const useStore = create(
devtools(
persist(
(set) => ({
theme: 'light',
toggleTheme: () => set((s) => ({ theme: s.theme === 'light' ? 'dark' : 'light' })),
}),
{ name: 'theme-storage' }
)
)
)How middleware is built internally
Middleware is a higher-order function:
type Middleware = (createState: StateCreator) => StateCreatorIt receives createState (your function that creates the store)
and returns a new function that wraps or changes the behavior of set, get, subscribe.
An example of a custom middleware:
const logger = (config) => (set, get, api) =>
config(
(args) => {
console.log('Before:', get())
set(args)
console.log('After:', get())
},
get,
api
)Summary
| What middleware does | Example |
|---|---|
| Adds new functionality to the store | persist, devtools, immer |
Intercepts and changes set() | logging, filtering |
| Extends the store's API | methods like .persist.rehydrate() |
| Can be combined | devtools(persist(...)) |
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.