Zustand - Middleware
Zustand provides powerful middleware to enhance store functionality including persistence, Redux DevTools integration, immutable updates with Immer, and more.
Key Concepts
Middleware Composition
Middleware wraps the store creator function:
import { create } from 'zustand' import { persist, devtools } from 'zustand/middleware'
const useStore = create( devtools( persist( (set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), }), { name: 'counter-storage' } ) ) )
Order Matters
Apply middleware from inside out:
// ✅ Correct order create(devtools(persist(immer(...))))
// devtools wraps persist wraps immer wraps your store
Best Practices
- Persist Middleware
Save and restore store state to localStorage or other storage:
import { create } from 'zustand' import { persist, createJSONStorage } from 'zustand/middleware'
interface CartStore { items: CartItem[] addItem: (item: CartItem) => void removeItem: (id: string) => void clearCart: () => void }
const useCartStore = create<CartStore>()( persist( (set) => ({ items: [], addItem: (item) => set((state) => ({ items: [...state.items, item] })), removeItem: (id) => set((state) => ({ items: state.items.filter((item) => item.id !== id), })), clearCart: () => set({ items: [] }), }), { name: 'shopping-cart', storage: createJSONStorage(() => localStorage), } ) )
Persist Options
persist( (set) => ({ /* store */ }), { name: 'my-store', // unique name for storage key storage: createJSONStorage(() => localStorage), // or sessionStorage partialize: (state) => ({ count: state.count }), // only persist specific fields onRehydrateStorage: (state) => { console.log('hydration starts') return (state, error) => { if (error) { console.log('error during hydration', error) } else { console.log('hydration finished') } } }, version: 1, migrate: (persistedState, version) => { // Handle version migrations if (version === 0) { // migrate old state to new format } return persistedState }, } )
- DevTools Middleware
Integrate with Redux DevTools for debugging:
import { create } from 'zustand' import { devtools } from 'zustand/middleware'
interface Store { count: number increment: () => void decrement: () => void }
const useStore = create<Store>()( devtools( (set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 }), false, 'increment'), decrement: () => set((state) => ({ count: state.count - 1 }), false, 'decrement'), }), { name: 'CounterStore' } ) )
DevTools Options
devtools( (set) => ({ /* store */ }), { name: 'MyStore', // name in devtools enabled: process.env.NODE_ENV === 'development', // enable conditionally anonymousActionType: 'action', // default action name trace: true, // include stack traces } )
- Immer Middleware
Write immutable updates with mutable syntax:
import { create } from 'zustand' import { immer } from 'zustand/middleware/immer'
interface TodoStore { todos: Todo[] addTodo: (text: string) => void toggleTodo: (id: string) => void updateTodo: (id: string, text: string) => void }
const useTodoStore = create<TodoStore>()( immer((set) => ({ todos: [],
addTodo: (text) =>
set((state) => {
state.todos.push({
id: Date.now().toString(),
text,
completed: false,
})
}),
toggleTodo: (id) =>
set((state) => {
const todo = state.todos.find((t) => t.id === id)
if (todo) {
todo.completed = !todo.completed
}
}),
updateTodo: (id, text) =>
set((state) => {
const todo = state.todos.find((t) => t.id === id)
if (todo) {
todo.text = text
}
}),
})) )
- Subscriptions
Listen to state changes outside React:
const useStore = create<Store>()((set) => ({ /* ... */ }))
// Subscribe to all changes const unsubscribe = useStore.subscribe((state, prevState) => { console.log('State changed:', state) })
// Subscribe to specific values const unsubscribe = useStore.subscribe( (state) => state.count, (count, prevCount) => { console.log('Count changed from', prevCount, 'to', count) } )
// Clean up unsubscribe()
- Combining Multiple Middleware
import { create } from 'zustand' import { devtools, persist } from 'zustand/middleware' import { immer } from 'zustand/middleware/immer'
interface Store { count: number todos: Todo[] increment: () => void addTodo: (text: string) => void }
const useStore = create<Store>()( devtools( persist( immer((set) => ({ count: 0, todos: [],
increment: () =>
set((state) => {
state.count++
}),
addTodo: (text) =>
set((state) => {
state.todos.push({
id: Date.now().toString(),
text,
completed: false,
})
}),
})),
{
name: 'app-storage',
partialize: (state) => ({
count: state.count,
todos: state.todos,
}),
}
),
{ name: 'AppStore' }
) )
Examples
Custom Logging Middleware
import { StateCreator, StoreMutatorIdentifier } from 'zustand'
type Logger = < T, Mps extends [StoreMutatorIdentifier, unknown][] = [], Mcs extends [StoreMutatorIdentifier, unknown][] = []
( f: StateCreator<T, Mps, Mcs>, name?: string ) => StateCreator<T, Mps, Mcs>
type LoggerImpl = <T>( f: StateCreator<T, [], []>, name?: string ) => StateCreator<T, [], []>
const loggerImpl: LoggerImpl = (f, name) => (set, get, store) => {
const loggedSet: typeof set = (...a) => {
set(...a)
console.log(...(name ? [${name}:] : []), get())
}
store.setState = loggedSet
return f(loggedSet, get, store) }
export const logger = loggerImpl as unknown as Logger
// Usage const useStore = create<Store>()( logger( (set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), }), 'CounterStore' ) )
Custom Reset Middleware
import { StateCreator, StoreMutatorIdentifier } from 'zustand'
type Resettable = < T, Mps extends [StoreMutatorIdentifier, unknown][] = [], Mcs extends [StoreMutatorIdentifier, unknown][] = []
( f: StateCreator<T, Mps, Mcs> ) => StateCreator<T, Mps, Mcs>
type ResettableImpl = <T>( f: StateCreator<T, [], []> ) => StateCreator<T, [], []>
const resettableImpl: ResettableImpl = (f) => (set, get, store) => { const initialState = f(set, get, store)
store.reset = () => set(initialState)
return initialState }
export const resettable = resettableImpl as unknown as Resettable
// Extend store type declare module 'zustand' { interface StoreApi<T> { reset?: () => void } }
// Usage const useStore = create<Store>()( resettable((set) => ({ count: 0, name: '', increment: () => set((state) => ({ count: state.count + 1 })), setName: (name) => set({ name }), })) )
// Reset to initial state useStore.reset()
IndexedDB Persistence
import { StateStorage } from 'zustand/middleware' import { get, set, del } from 'idb-keyval'
const indexedDBStorage: StateStorage = { getItem: async (name: string): Promise<string | null> => { return (await get(name)) || null }, setItem: async (name: string, value: string): Promise<void> => { await set(name, value) }, removeItem: async (name: string): Promise<void> => { await del(name) }, }
const useStore = create<Store>()( persist( (set) => ({ largeData: [], addData: (data) => set((state) => ({ largeData: [...state.largeData, data] })), }), { name: 'large-data-storage', storage: createJSONStorage(() => indexedDBStorage), } ) )
Async Storage for React Native
import AsyncStorage from '@react-native-async-storage/async-storage' import { StateStorage } from 'zustand/middleware'
const asyncStorage: StateStorage = { getItem: async (name: string): Promise<string | null> => { return await AsyncStorage.getItem(name) }, setItem: async (name: string, value: string): Promise<void> => { await AsyncStorage.setItem(name, value) }, removeItem: async (name: string): Promise<void> => { await AsyncStorage.removeItem(name) }, }
const useStore = create<Store>()( persist( (set) => ({ /* ... */ }), { name: 'app-storage', storage: createJSONStorage(() => asyncStorage), } ) )
Common Patterns
Conditional Persistence
Only persist certain fields:
const useStore = create<Store>()( persist( (set) => ({ // Persisted theme: 'light', language: 'en',
// Not persisted
isLoading: false,
error: null,
setTheme: (theme) => set({ theme }),
setLanguage: (language) => set({ language }),
}),
{
name: 'settings',
partialize: (state) => ({
theme: state.theme,
language: state.language,
}),
}
) )
Version Migration
Handle breaking changes in persisted state:
const useStore = create<Store>()( persist( (set) => ({ /* ... */ }), { name: 'app-store', version: 2, migrate: (persistedState: any, version: number) => { if (version === 0) { // Migrate from version 0 to 1 persistedState.newField = 'default' }
if (version === 1) {
// Migrate from version 1 to 2
persistedState.items = persistedState.oldItems.map((item: any) => ({
id: item.id,
name: item.title, // renamed field
}))
delete persistedState.oldItems
}
return persistedState as Store
},
}
) )
Hydration Detection
Know when persisted state is loaded:
const useStore = create<Store>()( persist( (set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), }), { name: 'counter', onRehydrateStorage: () => (state) => { console.log('State hydrated:', state) }, } ) )
// In a component function App() { const [hydrated, setHydrated] = useState(false)
useEffect(() => { useStore.persist.onFinishHydration(() => { setHydrated(true) }) }, [])
if (!hydrated) { return <div>Loading...</div> }
return <div>App content</div> }
Anti-Patterns
❌ Don't Persist Sensitive Data
// Bad: Persisting tokens in localStorage const useAuthStore = create( persist( (set) => ({ token: null, user: null, login: async (credentials) => { const { token, user } = await api.login(credentials) set({ token, user }) // ❌ Token in localStorage }, }), { name: 'auth' } ) )
// Good: Use secure storage or don't persist tokens const useAuthStore = create( persist( (set) => ({ user: null, login: async (credentials) => { const { token, user } = await api.login(credentials) secureStorage.setToken(token) // ✅ Secure storage set({ user }) }, }), { name: 'auth', partialize: (state) => ({ user: state.user }), // ✅ Only persist user } ) )
❌ Don't Ignore Middleware Order
// Bad: DevTools won't see persisted initial state create(persist(devtools(...)))
// Good: DevTools can see full state lifecycle create(devtools(persist(...)))
❌ Don't Mutate State Without Immer
// Bad: Mutating without immer const useStore = create((set) => ({ items: [], addItem: (item) => set((state) => { state.items.push(item) // ❌ Direct mutation return state }), }))
// Good: Use immer middleware const useStore = create( immer((set) => ({ items: [], addItem: (item) => set((state) => { state.items.push(item) // ✅ Safe with immer }), })) )
❌ Don't Forget to Clean Up Subscriptions
// Bad: Memory leak useEffect(() => { useStore.subscribe((state) => { console.log(state) }) }, [])
// Good: Clean up subscription useEffect(() => { const unsubscribe = useStore.subscribe((state) => { console.log(state) }) return unsubscribe }, [])
Related Skills
-
zustand-store-patterns: Basic store creation and usage
-
zustand-typescript: TypeScript integration with middleware
-
zustand-advanced-patterns: Custom middleware and advanced techniques