Skip to main content

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:

javascript
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:

javascript
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.


MiddlewarePurpose
devtoolsintegration with Redux DevTools for debugging
persistsaves state to localStorage, sessionStorage, or IndexedDB
immerlets you change state "imperatively" through a draft (like in Redux Toolkit)
subscribeWithSelectorsubscription to only the fields you need (deep selectivity)
combinehelps combine several state slices into one store
reduxemulates Redux-like behavior with actions and reducers

Usage examples

1. persist

Persists state across page reloads:

javascript
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:

javascript
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):

javascript
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:

javascript
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:

javascript
type Middleware = (createState: StateCreator) => StateCreator

It 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:

javascript
const logger = (config) => (set, get, api) => config( (args) => { console.log('Before:', get()) set(args) console.log('After:', get()) }, get, api )

Summary

What middleware doesExample
Adds new functionality to the storepersist, devtools, immer
Intercepts and changes set()logging, filtering
Extends the store's APImethods like .persist.rehydrate()
Can be combineddevtools(persist(...))

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.