Prompt directory
Impulse.directory
HomeBrowseStacks
Sign inCreate an account
    rule

    Zustand best practices

    impulse-lab/zustand-best-practicesยทv1ยทupdated September 25, 2025
    Leonard Roussard
    Leonard Roussard@lionvsxยทImpulse Lab
    Cursor / Rules
    Content
    ---
    description: Best practices for using Zustand for state management
    globs:
    alwaysApply: false
    ---
    
    # Zustand Best Practices
    Best practices for using Zustand for state management. Auto-included for store files.
    <rule>
    name: zustand_best_practices
    description: Best practices for using Zustand for state management. Auto-included for store files.
    globs: ["**/*store*.{ts,tsx,js,jsx}"]
    filters:
    - type: content
    pattern: "(?:import|from).*zustand"
    actions:
    - type: suggest
    message: |
        1. **Keep Stores Small and Focused**:
            - Unlike Redux, prefer multiple small stores over a single global store
            - Each store should be responsible for a single piece of functionality
            - Avoid storing unrelated state in the same store
            - For larger applications, create multiple small, focused stores instead of a single large one
            - This improves organization, reduces errors, and simplifies testing
        2. **Structure Store with Separated State and Actions Objects**:
            - Clearly separate state and actions into their own objects within the store
            - State should be a single object containing all state properties
            - Actions should be grouped in their own object
            - This pattern enables better organization and selective exports
        3. `// โœ… Good: Clear separation of state and actions
    const useStore = create((set, get) => {
      // State object
      const state = {
        count: 0,
        items: [],
      }
    
      // Actions object with all functions that modify state
      const actions = {
        increment: () => set((state) => ({ count: state.count + 1 })),
        addItem: (item) => set((state) => ({ items: [...state.items, item] })),
        resetAll: () => set({ count: 0, items: [] }),
      }
    
      return {
        ...state,
        actions,
      }
    })
    `
        4. **Create Atomic State Hooks and a Single Actions Hook**:
            - Export individual atomic hooks for each state property
            - Export a single hook for all actions
            - This prevents unnecessary re-renders while maintaining easy access to all actions
        5. `// Store implementation
    const useStore = create((set, get) => {
      const state = {
        bears: 0,
        fish: 0,
      }
    
      const actions = {
        increaseBears: () => set((state) => ({ bears: state.bears + 1 })),
        increaseFish: () => set((state) => ({ fish: state.fish + 1 })),
        feedBear: () => {
          const { bears, fish } = get()
          if (fish > 0) {
            set({ bears: bears + 1, fish: fish - 1 })
          }
        },
        resetAnimals: () => set({ bears: 0, fish: 0 }),
      }
    
      return {
        ...state,
        actions,
      }
    })
    
    // โœ… Good: Atomic selectors for state
    export const useBears = () => useStore((state) => state.bears)
    export const useFish = () => useStore((state) => state.fish)
    
    // โœ… Good: Single hook for all actions
    export const useStoreActions = () => useStore((state) => state.actions)
    
    // ๐Ÿค” Optional: If you need to split actions into groups
    import shallow from 'zustand/shallow'
    export const useBearActions = () => useStore(
      (state) => ({
        increaseBears: state.actions.increaseBears,
        feedBear: state.actions.feedBear
      }),
      shallow
    )
    `
        6. **Use **`shallow`** for Multi-Property State Selections**:
            - When you need to select multiple state properties, use `shallow` to prevent unnecessary re-renders
            - Import `shallow` from 'zustand/shallow' (or 'zustand/react/shallow' in newer versions)
            - This performs a shallow comparison of the selected object's properties instead of reference equality
            - Perfect for creating domain-specific hooks that use multiple state values
        7. `// Without shallow - will re-render on ANY state change
    // ๐Ÿšจ Bad: Component re-renders when unrelated state changes
    const UserProfile = () => {
      const { name, email, avatar } = useUserStore((state) => ({
        name: state.name,
        email: state.email,
        avatar: state.avatar
      }))
      // Will re-render even if other unrelated state changes
    }
    
    // With shallow - only re-renders when selected properties change
    // โœ… Good: Using shallow for multi-property selection
    import { shallow } from 'zustand/shallow'
    
    const UserProfile = () => {
      const { name, email, avatar } = useUserStore(
        (state) => ({
          name: state.name,
          email: state.email,
          avatar: state.avatar
        }),
        shallow
      )
      // Only re-renders when name, email, or avatar change
    }
    
    // โœ… Good: Create domain-specific hooks with shallow
    export const useUserProfileData = () => useUserStore(
      (state) => ({
        name: state.name,
        email: state.email,
        avatar: state.avatar,
        isVerified: state.isVerified
      }),
      shallow
    )
    
    // โœ… Alternative syntax - array selector with shallow
    const [name, email] = useUserStore(
      (state) => [state.name, state.email],
      shallow
    )
    `
        8. **Encapsulate Business Logic Within Store Actions**:
            - All business logic should live inside the store's actions, not outside
            - Actions should model domain events with all necessary transformations
            - Components should only need to call actions directly, not perform business logic
        9. `// ๐Ÿšจ Bad: Business logic outside the store
    const useTodoStore = create((set) => ({
      todos: [],
      setTodos: (todos) => set({ todos }),
    }))
    
    // Logic exists outside the store - BAD!
    function addTodo(todo) {
      const formattedTodo = {
        ...todo,
        id: Date.now(),
        createdAt: new Date().toISOString(),
        completed: false
      }
      const todos = useTodoStore.getState().todos
      useTodoStore.getState().setTodos([...todos, formattedTodo])
    }
    
    // โœ… Good: Business logic inside store actions
    const useTodoStore = create((set, get) => {
      const state = {
        todos: [],
      }
    
      const actions = {
        addTodo: (todo) => set((state) => ({
          todos: [...state.todos, {
            ...todo,
            id: Date.now(),
            createdAt: new Date().toISOString(),
            completed: false
          }]
        })),
        toggleTodo: (id) => set((state) => ({
          todos: state.todos.map(todo =>
            todo.id === id ? { ...todo, completed: !todo.completed } : todo
          )
        })),
        clearCompletedTodos: () => set((state) => ({
          todos: state.todos.filter(todo => !todo.completed)
        })),
      }
    
      return {
        ...state,
        actions,
      }
    })
    
    // Usage in component is simple - just call the action
    function TodoForm() {
      const { addTodo } = useStoreActions()
    
      const handleSubmit = (e) => {
        e.preventDefault()
        addTodo({ title: e.target.todo.value })
        e.target.reset()
      }
    
      return <form onSubmit={handleSubmit}>...</form>
    }
    `
        10. **Use Immutable Updates**:
            - Always update state immutably
            - Consider using immer for complex nested state updates
        11. `// With immer middleware
    import { immer } from 'zustand/middleware/immer'
    
    const useStore = create(
      immer((set) => ({
        nested: { structure: { value: 0 } },
        actions: {
          updateNested: () => set((state) => {
            state.nested.structure.value += 1
          })
        }
      }))
    )
    `
        12. **Type Your Stores**:
            - Split types into State, Actions, and Store
            - This improves clarity and type safety
        13. `interface TodoState {
      todos: Todo[]
    }
    
    interface TodoActions {
      addTodo: (todo: Omit<Todo, 'id' | 'createdAt' | 'completed'>) => void
      toggleTodo: (id: number) => void
      clearCompletedTodos: () => void
    }
    
    // The store combines state and the actions object
    interface TodoStore extends TodoState {
      actions: TodoActions
    }
    
    const useTodoStore = create<TodoStore>((set) => {
      const state: TodoState = {
        todos: [],
      }
    
      const actions: TodoActions = {
        addTodo: (todo) => set((state) => ({
          todos: [...state.todos, {
            ...todo,
            id: Date.now(),
            createdAt: new Date().toISOString(),
            completed: false
          }]
        })),
        toggleTodo: (id) => set((state) => ({
          todos: state.todos.map(todo =>
            todo.id === id ? { ...todo, completed: !todo.completed } : todo
          )
        })),
        clearCompletedTodos: () => set((state) => ({
          todos: state.todos.filter(todo => !todo.completed)
        })),
      }
    
      return {
        ...state,
        actions,
      }
    })
    `
    examples:
    - input: |
    // Bad: Everything in one big store
    const useGlobalStore = create((set) => ({
    user: null,
    posts: [],
    comments: [],
    likes: {},
    notifications: [],
    settings: {},
    // many more unrelated pieces of state...
    }))
    - // Good: Separate stores for separate concerns
    const useUserStore = create((set) => ({
    user: null,
    actions: {
    login: (user) => set({ user }),
    logout: () => set({ user: null }),
    }
    }))
    - const usePostsStore = create((set) => ({
    posts: [],
    actions: {
    addPost: (post) => set((state) => ({
    posts: [...state.posts, post]
    })),
    }
    }))
    output: "Split large stores into smaller, more focused ones"
    - input: |
    // Bad: No clear separation of state and actions
    const useBadStore = create((set) => ({
    bears: 0,
    fish: 0,
    increaseBears: () => set((state) => ({ bears: state.bears + 1 })),
    increaseFish: () => set((state) => ({ fish: state.fish + 1 })),
    }))
    - // Bad: Using the store without atomic selectors
    function Component() {
    const { bears, increaseBears } = useBadStore()
    return <div onClick={increaseBears}>{bears}</div>
    }
    - // Good: Clear separation and atomic selectors
    const useGoodStore = create((set) => {
    const state = {
    bears: 0,
    fish: 0,
    }
    - `const actions = {
      increaseBears: () => set((state) => ({ bears: state.bears + 1 })),
      increaseFish: () => set((state) => ({ fish: state.fish + 1 })),
    }
    
    return { ...state, actions }
    `
    - })
    - // Export atomic state selectors
    export const useBears = () => useGoodStore((state) => state.bears)
    export const useFish = () => useGoodStore((state) => state.fish)
    - // Export actions as a single hook
    export const useAnimalActions = () => useGoodStore((state) => state.actions)
    - // Good usage in components
    function BetterComponent() {
    const bears = useBears()
    const { increaseBears } = useAnimalActions()
    - `return <div onClick={increaseBears}>{bears}</div>
    `
    - }
    output: "Use atomic state selectors and group actions"
    - input: |
    // Bad: Business logic outside the store
    const useCartStore = create((set) => ({
    items: [],
    setItems: (items) => set({ items }),
    }))
    - // Business logic in a component or utility
    function addToCart(product, quantity = 1) {
    const cart = useCartStore.getState()
    const existingItem = cart.items.find(item => item.id === product.id)
    - `if (existingItem) {
      const updatedItems = cart.items.map(item =>
        item.id === product.id
          ? { ...item, quantity: item.quantity + quantity }
          : item
      )
      cart.setItems(updatedItems)
    } else {
      cart.setItems([...cart.items, { ...product, quantity }])
    }
    `
    - }
    - // Good: Business logic inside store actions
    const useCartStore = create((set, get) => {
    const state = {
    items: [],
    }
    - `const actions = {
      addToCart: (product, quantity = 1) => set((state) => {
        const existingItem = state.items.find(item => item.id === product.id)
    
        if (existingItem) {
          return {
            items: state.items.map(item =>
              item.id === product.id
                ? { ...item, quantity: item.quantity + quantity }
                : item
            )
          }
        } else {
          return {
            items: [...state.items, { ...product, quantity }]
          }
        }
      }),
    
      removeFromCart: (productId) => set((state) => ({
        items: state.items.filter(item => item.id !== productId)
      })),
    
      clearCart: () => set({ items: [] }),
    }
    
    return {
      ...state,
      actions,
    }
    `
    - })
    - // Export selectors and actions
    export const useCartItems = () => useCartStore((state) => state.items)
    export const useCartTotal = () => useCartStore((state) =>
    state.items.reduce((total, item) => total + (item.price * item.quantity), 0)
    )
    export const useCartActions = () => useCartStore((state) => state.actions)
    - // Usage in component
    function AddToCartButton({ product }) {
    const { addToCart } = useCartActions()
    return <button onClick={() => addToCart(product)}>Add to Cart</button>
    }
    output: "Encapsulate business logic within store actions"
    - input: |
    // Bad: Multi-property selection without shallow (causes unnecessary re-renders)
    const ProfileComponent = () => {
    // This will re-render even if unrelated state changes
    const { firstName, lastName, email } = useUserStore(state => ({
    firstName: state.firstName,
    lastName: state.lastName,
    email: state.email
    }))
    - `return <div>{firstName} {lastName}: {email}</div>
    `
    - }
    - // Good: Multi-property selection with shallow
    import { shallow } from 'zustand/shallow'
    - const ProfileComponent = () => {
    // This only re-renders when firstName, lastName, or email change
    const { firstName, lastName, email } = useUserStore(
    state => ({
    firstName: state.firstName,
    lastName: state.lastName,
    email: state.email
    }),
    shallow
    )
    - `return <div>{firstName} {lastName}: {email}</div>
    `
    - }
    - // Even better: Domain-specific hook with shallow
    export const useUserProfile = () => useUserStore(
    state => ({
    firstName: state.firstName,
    lastName: state.lastName,
    email: state.email,
    fullName: `${state.firstName} ${state.lastName}`
    }),
    shallow
    )
    - // Usage in component
    const ProfileComponent = () => {
    const { firstName, lastName, email, fullName } = useUserProfile()
    return <div>{fullName}: {email}</div>
    }
    output: "Use shallow for selecting multiple state properties efficiently"
    metadata:
    priority: high
    version: 1.2
    </rule>

    Install with the impulse CLI

    $npx @impulselab/directory impulselab/zustand-best-practices

    Stats

    Views
    10
    Saves
    1

    Owner

    impulse-logo

    Impulse Lab

    Official artifact