Skill v1.0.1
currentLLM-judged scan100/1003 files
version: "1.0.1" name: zustand-middleware description: Use when implementing Zustand middleware for persistence, dev tools, immutability, and other enhanced store functionality. Covers persist, devtools, immer, and custom middleware. allowed-tools:
- Read
- Write
- Edit
- Bash
- Grep
- Glob
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 ordercreate(devtools(persist(immer(...))))// devtools wraps persist wraps immer wraps your store
Best Practices
1. 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) => voidremoveItem: (id: string) => voidclearCart: () => 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 keystorage: createJSONStorage(() => localStorage), // or sessionStoragepartialize: (state) => ({ count: state.count }), // only persist specific fieldsonRehydrateStorage: (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 migrationsif (version === 0) {// migrate old state to new format}return persistedState},})
2. DevTools Middleware
Integrate with Redux DevTools for debugging:
import { create } from 'zustand'import { devtools } from 'zustand/middleware'interface Store {count: numberincrement: () => voiddecrement: () => 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 devtoolsenabled: process.env.NODE_ENV === 'development', // enable conditionallyanonymousActionType: 'action', // default action nametrace: true, // include stack traces})
3. 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) => voidtoggleTodo: (id: string) => voidupdateTodo: (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}}),})))
4. Subscriptions
Listen to state changes outside React:
const useStore = create<Store>()((set) => ({ /* ... */ }))// Subscribe to all changesconst unsubscribe = useStore.subscribe((state, prevState) => {console.log('State changed:', state)})// Subscribe to specific valuesconst unsubscribe = useStore.subscribe((state) => state.count,(count, prevCount) => {console.log('Count changed from', prevCount, 'to', count)})// Clean upunsubscribe()
5. Combining Multiple Middleware
import { create } from 'zustand'import { devtools, persist } from 'zustand/middleware'import { immer } from 'zustand/middleware/immer'interface Store {count: numbertodos: Todo[]increment: () => voidaddTodo: (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 = loggedSetreturn f(loggedSet, get, store)}export const logger = loggerImpl as unknown as Logger// Usageconst 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 typedeclare module 'zustand' {interface StoreApi<T> {reset?: () => void}}// Usageconst useStore = create<Store>()(resettable((set) => ({count: 0,name: '',increment: () => set((state) => ({ count: state.count + 1 })),setName: (name) => set({ name }),})))// Reset to initial stateuseStore.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) => ({// Persistedtheme: 'light',language: 'en',// Not persistedisLoading: 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 1persistedState.newField = 'default'}if (version === 1) {// Migrate from version 1 to 2persistedState.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 componentfunction 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 localStorageconst 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 tokensconst useAuthStore = create(persist((set) => ({user: null,login: async (credentials) => {const { token, user } = await api.login(credentials)secureStorage.setToken(token) // ✅ Secure storageset({ user })},}),{name: 'auth',partialize: (state) => ({ user: state.user }), // ✅ Only persist user}))
❌ Don't Ignore Middleware Order
// Bad: DevTools won't see persisted initial statecreate(persist(devtools(...)))// Good: DevTools can see full state lifecyclecreate(devtools(persist(...)))
❌ Don't Mutate State Without Immer
// Bad: Mutating without immerconst useStore = create((set) => ({items: [],addItem: (item) =>set((state) => {state.items.push(item) // ❌ Direct mutationreturn state}),}))// Good: Use immer middlewareconst 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 leakuseEffect(() => {useStore.subscribe((state) => {console.log(state)})}, [])// Good: Clean up subscriptionuseEffect(() => {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