Skip to main content
Updating state in Zustand is done through the set function. Understanding how to update state properly is crucial for maintaining predictable application behavior.

The set Function

The set function is provided to your state creator and is used to update the store’s state:
type SetStateInternal<T> = {
  (
    partial: T | Partial<T> | ((state: T) => T | Partial<T>),
    replace?: false,
  ): void
  (state: T | ((state: T) => T), replace: true): void
}

Partial Updates (Shallow Merge)

By default, set performs a shallow merge of the new state with the existing state:
import { create } from 'zustand'

type State = {
  firstName: string
  lastName: string
  age: number
}

const usePersonStore = create<State>()((set) => ({
  firstName: 'John',
  lastName: 'Doe',
  age: 30,
}))

// Only updates firstName, keeping lastName and age unchanged
usePersonStore.setState({ firstName: 'Jane' })
Shallow merge means that set merges properties at the top level only. Nested objects must be merged manually.

Function Updates

When the new state depends on the previous state, use a function:
type CounterStore = {
  count: number
  increment: () => void
}

const useCounterStore = create<CounterStore>()((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}))
Always use function updates when the new state depends on the previous state. This ensures you’re working with the latest state, especially important in async scenarios.

Replace Mode

Use the second parameter of set to completely replace the state instead of merging:
const useStore = create<{ a: number } | { b: number }>()(() => ({ a: 1 }))

// Merge mode (default)
useStore.setState({ b: 2 }) // Result: { a: 1, b: 2 }

// Replace mode
useStore.setState({ b: 2 }, true) // Result: { b: 2 }
Replace mode discards all existing state. Use with caution to avoid losing nested data.

Immutable Update Patterns

Zustand requires immutable updates. Never mutate state directly.

Updating Primitives

1

Numbers, Strings, Booleans

Simply assign the new value:
type State = {
  count: number
  name: string
  active: boolean
}

const useStore = create<State>()((set) => ({
  count: 0,
  name: '',
  active: false,
}))

// Direct assignment works for primitives
useStore.setState({ count: 1 })
useStore.setState({ name: 'Alice' })
useStore.setState({ active: true })

Updating Objects

type State = {
  user: { name: string; age: number }
  updateUser: (updates: Partial<State['user']>) => void
}

const useStore = create<State>()((set) => ({
  user: { name: 'John', age: 30 },
  updateUser: (updates) =>
    set((state) => ({
      user: { ...state.user, ...updates },
    })),
}))

// Usage
useStore.getState().updateUser({ age: 31 })
This is wrong - it mutates state:
// ❌ BAD: Direct mutation
set((state) => {
  state.user.name = 'Jane'
  return state
})

// ✅ GOOD: Immutable update
set((state) => ({
  user: { ...state.user, name: 'Jane' },
}))

Updating Arrays

type TodoStore = {
  todos: string[]
  addTodo: (todo: string) => void
}

const useStore = create<TodoStore>()((set) => ({
  todos: [],
  addTodo: (todo) =>
    set((state) => ({ todos: [...state.todos, todo] })),
}))
Use the spread operator [...array, newItem] or concat() to add items.

Immutable Operations Reference

  • Arrays: Direct assignment array[i] = x, push(), unshift(), pop(), shift(), splice(), reverse(), sort()
  • Objects: Direct property assignment object.prop = x

Deep State Updates

For complex nested state, consider using a library:
type State = {
  deep: {
    nested: {
      obj: { count: number }
    }
  }
}

const useStore = create<State>()((set) => ({
  deep: { nested: { obj: { count: 0 } } },
}))

// Manual deep update
useStore.setState((state) => ({
  deep: {
    ...state.deep,
    nested: {
      ...state.deep.nested,
      obj: {
        ...state.deep.nested.obj,
        count: state.deep.nested.obj.count + 1,
      },
    },
  },
}))

Updating State Outside Actions

You can define actions outside the store:
const useStore = create<{ count: number }>()(() => ({ count: 0 }))

// External action
const increment = () => {
  useStore.setState((state) => ({ count: state.count + 1 }))
}

// Use in components
function Counter() {
  const count = useStore((state) => state.count)
  return <button onClick={increment}>{count}</button>
}
While this pattern works, it’s generally recommended to colocate actions with state inside the store for better organization.

Batching Updates

Zustand automatically batches state updates in React event handlers:
import { create } from 'zustand'

const useStore = create<{ count: number }>()(() => ({ count: 0 }))

function Component() {
  const handleClick = () => {
    // These are automatically batched
    useStore.setState((s) => ({ count: s.count + 1 }))
    useStore.setState((s) => ({ count: s.count + 1 }))
    // Component only re-renders once with count: 2
  }

  return <button onClick={handleClick}>Increment</button>
}

Common Pitfalls

Direct Mutation
// ❌ BAD: Mutates state
set((state) => {
  state.count++
  return state
})

// ✅ GOOD: Returns new state
set((state) => ({ count: state.count + 1 }))
Forgetting to Return
// ❌ BAD: Arrow function without return
const increment = () => set((state) => { count: state.count + 1 })

// ✅ GOOD: With explicit return or implicit return
const increment = () => set((state) => ({ count: state.count + 1 }))
Using set Instead of setState
// Inside the store creator
(set) => ({
  increment: () => set((s) => ({ count: s.count + 1 })), // ✅ Correct
})

// Outside the store
const increment = () => {
  useStore.setState((s) => ({ count: s.count + 1 })) // ✅ Correct
}

Next Steps

Async Actions

Handle asynchronous operations in your store

Vanilla Stores

Use Zustand without React

Build docs developers (and LLMs) love