Skip to main content
Map and Set are mutable data structures. To use them correctly in Zustand, you must create new instances when updating to ensure proper reactivity.

Working with Maps

Reading a Map

Reading from a Map is straightforward:
const foo = useSomeStore((state) => state.foo)

Updating a Map

Always create a new Map instance when updating. Mutating the existing Map won’t trigger re-renders because the reference doesn’t change.
// Update a single entry
set((state) => ({
  foo: new Map(state.foo).set(key, value),
}))

Working with Sets

Reading a Set

Reading from a Set is straightforward:
const bar = useSomeStore((state) => state.bar)

Updating a Set

Just like Maps, always create a new Set instance when updating.
// Add an item
set((state) => ({
  bar: new Set(state.bar).add(item),
}))

Why New Instances?

Zustand detects changes by comparing references using Object.is. Mutating a Map or Set in place doesn’t change its reference:
// This won't trigger a re-render
set((state) => {
  state.foo.set(key, value)
  return { foo: state.foo }  // Same reference!
})
When you create new Map(state.foo), it creates a shallow copy of the Map with a new reference. This new reference tells Zustand that the state has changed.

Type Hints for Empty Collections

When initializing empty Maps and Sets, provide type hints to prevent TypeScript from inferring never[]:
interface State {
  ids: Set<string>
  users: Map<string, User>
}

const useStore = create<State>()((set) => ({
  // Without type hints, TypeScript infers never[] - can't add items later
  // ids: new Set([]),  // Bad!
  // users: new Map([]), // Bad!

  // With type hints, TypeScript knows the correct types
  ids: new Set([] as string[]),  // Good!
  users: new Map([] as [string, User][]),  // Good!
}))
Without proper type hints, TypeScript will infer never[] which prevents adding items to the collection later.

Complete Example

Here’s a complete example showing Maps and Sets in action:
import { create } from 'zustand'

interface User {
  id: string
  name: string
}

interface Store {
  // Map of user IDs to user objects
  users: Map<string, User>
  addUser: (user: User) => void
  removeUser: (id: string) => void
  updateUser: (id: string, updates: Partial<User>) => void

  // Set of selected user IDs
  selectedIds: Set<string>
  toggleSelection: (id: string) => void
  clearSelection: () => void
}

const useStore = create<Store>()((set) => ({
  users: new Map([] as [string, User][]),
  selectedIds: new Set([] as string[]),

  addUser: (user) =>
    set((state) => ({
      users: new Map(state.users).set(user.id, user),
    })),

  removeUser: (id) =>
    set((state) => {
      const users = new Map(state.users)
      users.delete(id)
      return { users }
    }),

  updateUser: (id, updates) =>
    set((state) => {
      const user = state.users.get(id)
      if (!user) return state
      return {
        users: new Map(state.users).set(id, { ...user, ...updates }),
      }
    }),

  toggleSelection: (id) =>
    set((state) => {
      const selectedIds = new Set(state.selectedIds)
      selectedIds.has(id) ? selectedIds.delete(id) : selectedIds.add(id)
      return { selectedIds }
    }),

  clearSelection: () => set({ selectedIds: new Set([] as string[]) }),
}))

Live Demo

StackBlitz Demo

See a working example of Maps and Sets in Zustand
Remember: The key principle is always create new instances when updating Maps and Sets. This ensures Zustand can detect changes and trigger re-renders appropriately.

Build docs developers (and LLMs) love