DevTools & Persistence
Two of Zustand's most useful middleware: devtools for debugging with Redux DevTools, and persist for saving state across page refreshes.
Redux DevTools
Zustand integrates with the Redux DevTools browser extension out of the box.
# Install Redux DevTools Extension in Chrome/Firefox
# https://chrome.google.com/webstore/detail/redux-devtools/
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
interface CounterState {
count: number;
increment: () => void;
decrement: () => void;
reset: () => void;
}
export const useCounterStore = create<CounterState>()(
devtools(
(set) => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 }), false, 'increment'),
decrement: () => set(state => ({ count: state.count - 1 }), false, 'decrement'),
reset: () => set({ count: 0 }, false, 'reset'),
}),
{ name: 'CounterStore' } // appears in DevTools
)
);
The third argument to set is the action name — shown in DevTools time-travel.
persist Middleware
Save and rehydrate state from localStorage, sessionStorage, or any custom storage:
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
interface ThemeState {
theme: 'light' | 'dark' | 'system';
fontSize: 'sm' | 'md' | 'lg';
setTheme: (theme: ThemeState['theme']) => void;
setFontSize: (size: ThemeState['fontSize']) => void;
}
export const useThemeStore = create<ThemeState>()(
persist(
(set) => ({
theme: 'system',
fontSize: 'md',
setTheme: (theme) => set({ theme }),
setFontSize: (fontSize) => set({ fontSize }),
}),
{
name: 'theme-preferences', // localStorage key
storage: createJSONStorage(() => localStorage),
}
)
);
Persist Only Some Fields
Use partialize to exclude sensitive data:
export const useUserStore = create<UserState>()(
persist(
(set) => ({
user: null,
token: null,
preferences: { notifications: true, darkMode: false },
setUser: (user) => set({ user }),
setToken: (token) => set({ token }),
}),
{
name: 'user-storage',
partialize: (state) => ({
preferences: state.preferences,
// user and token NOT persisted (sensitive + handled by cookies)
}),
}
)
);
sessionStorage
For state that should clear when the browser closes:
persist(
(set) => ({ /* ... */ }),
{
name: 'session-data',
storage: createJSONStorage(() => sessionStorage),
}
)
Custom Storage (IndexedDB)
import { get, set, del } from 'idb-keyval';
const idbStorage = {
getItem: async (name: string) => (await get(name)) ?? null,
setItem: (name: string, value: string) => set(name, value),
removeItem: (name: string) => del(name),
};
persist(
(set) => ({ /* ... */ }),
{
name: 'large-data',
storage: idbStorage,
}
)
Combining devtools + persist
Middleware is composed with each wrapping the next:
export const useCartStore = create<CartState>()(
devtools(
persist(
(set, get) => ({
items: [],
addItem: (item) => set(s => ({ items: [...s.items, item] }), false, 'cart/addItem'),
clearCart: () => set({ items: [] }, false, 'cart/clear'),
}),
{ name: 'cart' }
),
{ name: 'CartStore' }
)
);
Handling Hydration in Next.js
localStorage doesn't exist on the server, causing hydration mismatches. Fix with a mounted check:
'use client';
import { useState, useEffect } from 'react';
export function CartIcon() {
const items = useCartStore(s => s.items);
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
// Don't render count until client-side hydration is complete
if (!mounted) return <ShoppingCartIcon />;
return (
<div className="relative">
<ShoppingCartIcon />
{items.length > 0 && (
<span className="badge">{items.length}</span>
)}
</div>
);
}
Or use Zustand's built-in skipHydration:
persist(/* ... */, {
name: 'cart',
skipHydration: true, // manually call rehydrate
})
// In your app layout:
useEffect(() => {
useCartStore.persist.rehydrate();
}, []);