Middleware Zustand

VérifiéSûr

Implémentez des middlewares Zustand pour la persistance, les outils de développement, l'immuabilité et d'autres fonctionnalités avancées. Couvre persist, devtools, immer et les middlewares personnalisés.

Spar Skills Guide Bot
DeveloppementIntermédiaire
11/03/2026
Claude CodeCursorWindsurfCopilotCodex
#zustand#state-management#middleware#react#typescript

Recommandé pour

Synthèse automatique

Ce skill documente la mise en place des middlewares Zustand (persist, devtools, immer) et leur composition dans un store React typé.

Points forts

  • Montre l'ordre de composition des middlewares avec un exemple correct et un exemple erroné côte à côte
  • Détaille les options de persist : partialize, version, migrate et callbacks de réhydratation
  • Couvre les options de devtools comme enabled, anonymousActionType et trace
  • Fournit des exemples TypeScript complets avec create<T>() et interfaces de store

Limites

  • Le contenu est tronqué en pleine section Immer, ce qui laisse le troisième middleware à peine esquissé
  • Les middlewares personnalisés sont annoncés dans la description mais aucun exemple n'est fourni
  • Peu d'indications sur les pièges de réhydratation côté SSR ou sur la combinaison avec d'autres bibliothèques
Quand l'utiliser

À utiliser lorsqu'on ajoute de la persistance, du débogage Redux DevTools ou des mises à jour Immer à un store Zustand existant.

Quand l'éviter

À éviter si l'on cherche des conseils sur les middlewares Zustand personnalisés ou une stratégie de test, absents de ce document.

Analyse de sécurité

Sûr
Score qualité90/100

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

Aucun point d'attention détecté

Comment cette analyse est faite

Exemples

Persist a cart store
Add persistence to my Zustand cart store so items survive a page reload, persisting only the items array under the key 'shopping-cart'.
Compose devtools and immer
Refactor my Zustand store to use devtools and immer middleware together, with named actions for increment and decrement.
Migrate persisted state
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

Du fichier au premier essai

Le téléchargement contient exactement le contenu affiché sur cette fiche. Cette copie peut différer de la dernière version du dépôt source.

Consulter la source et ses fichiers annexes

Ce téléchargement contient uniquement SKILL.md. Les scripts, références, modèles et dépendances éventuels ne sont pas inclus. Récupérez les fichiers nécessaires depuis la source en conservant leur arborescence avant de poursuivre.

1. Télécharger le fichier

Télécharger SKILL.md

Depuis la racine d’un projet de test, enregistrez le fichier sous .skill-downloads/zustand-middleware-mmlj2o5p/SKILL.md, ou utilisez la commande suivante. Ce dossier sert à examiner la skill avant son installation.

Commande pour macOS/Linux ou WSL : un shell POSIX et curl sont nécessaires. Sous Windows sans WSL, utilisez le bouton de téléchargement et créez le dossier manuellement.

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. Vérifier les prérequis

Lisez SKILL.md avant de l’utiliser. Si le fichier cite des scripts ou services externes, examinez-les et préparez les accès nécessaires dans votre environnement de test.

Aucune section de prérequis reconnue dans ce fichier. Cela ne signifie pas qu’il est autonome : les bibliothèques, outils et accès peuvent être cités ailleurs dans le contenu ou dans le dépôt source.

Les plateformes de la fiche sont des indications du catalogue. Les chemins ci-dessous suivent la documentation des outils ; ils ne prouvent pas que cette skill y a été exécutée. Aucune version minimale de ces outils n’est attestée pour cette fiche.

3. Charger la skill

Après vérification, copiez le dossier complet dans le projet avec l’une des commandes ci-dessous, puis ouvrez une nouvelle session de l’outil. La commande s’arrête si un dossier du même nom existe déjà : comparez les fichiers avant de le remplacer.

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 doit déjà être installé. La procédure locale install/read est contrôlée avec OpenSkills 1.5.0, qui exige Node.js ≥ 20.6.0. Aucun slug du catalogue n’est résolu automatiquement.

Installation officielle d’OpenSkills
openskills install './.skill-downloads/zustand-middleware-mmlj2o5p'

openskills read 'zustand-middleware-mmlj2o5p'

La lecture doit afficher le contenu du fichier installé et son dossier de base. Elle ne lance ni la skill ni ses dépendances.

Pour un agent qui lit AGENTS.md, synchronisez ensuite l’index. Cette commande modifie AGENTS.md : relisez le diff avant de l’utiliser.

openskills sync

Mode manuel ou autre outil : joignez SKILL.md à une conversation, ou ouvrez-le dans un agent capable de lire les fichiers. Ajoutez les ressources annexes nécessaires. Il n’existe pas de dossier universel reconnu par tous les outils.

4. Faire un premier essai

Utilisez une entrée courte et fictive : un endpoint d’API, un petit document ou un brief d’interface. Dans une nouvelle conversation, joignez le fichier ou donnez son chemin à l’agent, puis adaptez cette demande :

Lis .skill-downloads/zustand-middleware-mmlj2o5p/SKILL.md et applique ses instructions à cet exemple : [décrire une entrée de test]. Indique les fichiers, dépendances ou accès qui manquent. Produis le résultat demandé par la skill sans inventer de données ni exécuter de script non examiné.

Résultat attendu : une sortie correspondant aux instructions de la skill, ou une liste précise des prérequis manquants. Une simple confirmation de lecture ne valide pas le premier usage. Si une ressource externe est indisponible, ce parcours reste à tester.

Skills similaires