Automated summary
This skill documents how to set up Zustand middleware (persist, devtools, immer) and compose them inside a typed React store.
Strengths
- Shows middleware composition order with a correct example next to a wrong one
- Explains persist options in depth: partialize, version, migrate and rehydration callbacks
- Covers devtools options such as enabled, anonymousActionType and trace
- Provides complete TypeScript examples using create<T>() and store interfaces
Limitations
- The content is cut off mid-way through the Immer section, leaving that middleware barely sketched
- Custom middleware is promised in the description but no example is provided
- Little guidance on SSR rehydration pitfalls or on combining with other libraries
Use it when adding persistence, Redux DevTools debugging or Immer updates to an existing Zustand store.
Avoid it if you need advice on custom Zustand middleware or a testing strategy, which this document does not cover.
Security analysis
SafeThe skill is a documentation-style guide for Zustand middleware. Although Bash is declared as an allowed tool, the content contains only TypeScript examples and no destructive, exfiltration, obfuscation, or safety-disabling instructions.
No concerns found
How this analysis is madeExamples
Add persistence to my Zustand cart store so items survive a page reload, persisting only the items array under the key 'shopping-cart'.Refactor my Zustand store to use devtools and immer middleware together, with named actions for increment and decrement.My Zustand persisted store changed shape — add a version and a migrate function so old localStorage state is upgraded to the new format.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 order
create(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) => 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
},
}
)
2. 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
}
)
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) => 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
}
}),
}))
)
4. 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()
5. 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
From the file to a first result
The download contains exactly the content displayed on this page. This copy may differ from the latest version in the source repository.
View the source and supporting filesThis download contains SKILL.md only. Any scripts, references, templates and dependencies are not included. Get the required files from the source, preserving their directory structure, before continuing.
1. Download the file
Download SKILL.mdFrom the root of a test project, save the file as .skill-downloads/zustand-middleware-mmlj2o5p/SKILL.md, or use the command below. This folder is for inspecting the skill before installation.
Command for macOS/Linux or WSL: requires a POSIX shell and curl. On Windows without WSL, use the download button and create the folder manually.
mkdir -p '.skill-downloads/zustand-middleware-mmlj2o5p' && curl --fail --show-error --location 'https://skills-guides.com/api/skills/zustand-middleware-mmlj2o5p/download' --output '.skill-downloads/zustand-middleware-mmlj2o5p/SKILL.md.part' && mv '.skill-downloads/zustand-middleware-mmlj2o5p/SKILL.md.part' '.skill-downloads/zustand-middleware-mmlj2o5p/SKILL.md'2. Check prerequisites
Read SKILL.md before using it. If it references scripts or external services, inspect them and prepare the required access in your test environment.
No recognized prerequisites section in this file. This does not mean it is self-contained: libraries, tools and access requirements may be mentioned elsewhere in the content or source repository.
The platforms on this page are catalog indications. The paths below follow tool documentation; they do not prove this skill has been run in those tools. No minimum tool version is confirmed for this listing.
3. Load the skill
After inspection, copy the complete folder into your project using one of the commands below, then open a new tool session. The command stops if a folder with the same name already exists: compare the files before replacing it.
Claude Code
test -f '.skill-downloads/zustand-middleware-mmlj2o5p/SKILL.md' && test -s '.skill-downloads/zustand-middleware-mmlj2o5p/SKILL.md' && mkdir -p '.claude/skills' && test ! -e '.claude/skills/zustand-middleware' && cp -R '.skill-downloads/zustand-middleware-mmlj2o5p' '.claude/skills/zustand-middleware'Cursor
test -f '.skill-downloads/zustand-middleware-mmlj2o5p/SKILL.md' && test -s '.skill-downloads/zustand-middleware-mmlj2o5p/SKILL.md' && mkdir -p '.cursor/skills' && test ! -e '.cursor/skills/zustand-middleware' && cp -R '.skill-downloads/zustand-middleware-mmlj2o5p' '.cursor/skills/zustand-middleware'Codex
test -f '.skill-downloads/zustand-middleware-mmlj2o5p/SKILL.md' && test -s '.skill-downloads/zustand-middleware-mmlj2o5p/SKILL.md' && mkdir -p '.agents/skills' && test ! -e '.agents/skills/zustand-middleware' && cp -R '.skill-downloads/zustand-middleware-mmlj2o5p' '.agents/skills/zustand-middleware'OpenSkills
OpenSkills must already be installed. The local install/read procedure is checked with OpenSkills 1.5.0, which requires Node.js ≥ 20.6.0. Catalog slugs are not resolved automatically.
Official OpenSkills installationopenskills install './.skill-downloads/zustand-middleware-mmlj2o5p'openskills read 'zustand-middleware-mmlj2o5p'Reading should display the installed file content and its base directory. It does not run the skill or install its dependencies.
For an agent that reads AGENTS.md, then sync the index. This command changes AGENTS.md: review the diff before using it.
openskills syncManual method or another tool: attach SKILL.md to a conversation, or open it in an agent that can read files. Include any required supporting resources. No universal folder is recognized by every tool.
4. Try a first task
Use a short, synthetic input: an API endpoint, a small document or an interface brief. In a new conversation, attach the file or give the agent its path, then adapt this request:
Read .skill-downloads/zustand-middleware-mmlj2o5p/SKILL.md and apply its instructions to this example: [describe a test input]. Identify any missing files, dependencies or access. Produce the result requested by the skill without inventing data or running unreviewed scripts.Expected result: output matching the skill instructions, or a precise list of missing prerequisites. A read confirmation alone does not validate a first use. If an external resource is unavailable, that path remains untested.
React Hook Form Best Practices
Development
Master React Hook Form: avoid anti-patterns, manage subscriptions, and handle reset, dirty state, and controlled inputs correctly.
Emotion to Mantine Migration
Development
Migrate Emotion styled-components to Mantine components with style props and CSS modules. Use when converting .styled.tsx files or removing @emotion imports.
Scope Creep Detector
Development
Analyzes git diffs against a stated intent to detect scope creep, unrelated files, broad PRs, and oversized changes, then suggests keep, split, or justify actions.