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:
Simple Increment
Complex Update
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:
Object State
Primitive State
Array State
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.
const useCountStore = create < number >()(() => 0 )
// Always use replace mode for primitives
useCountStore . setState ( 1 , true )
useCountStore . setState (( count ) => count + 1 , true )
When your state is a primitive value (number, string, boolean), you must use replace: true.
type PositionStore = [ number , number ]
const usePositionStore = create < PositionStore >()(() => [ 0 , 0 ])
// Always use replace mode for arrays as the root state
usePositionStore . setState ([ 10 , 20 ], true )
Immutable Update Patterns
Zustand requires immutable updates. Never mutate state directly.
Updating Primitives
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
Shallow Object
Nested Object
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
Adding Items
Removing Items
Updating Items
Sorting & Reversing
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.
type TodoStore = {
todos : Array <{ id : number ; text : string }>
removeTodo : ( id : number ) => void
}
const useStore = create < TodoStore >()(( set ) => ({
todos: [],
removeTodo : ( id ) =>
set (( state ) => ({
todos: state . todos . filter (( todo ) => todo . id !== id ),
})),
}))
Use filter() to remove items.
type TodoStore = {
todos : Array <{ id : number ; text : string ; done : boolean }>
toggleTodo : ( id : number ) => void
updateTodoText : ( id : number , text : string ) => void
}
const useStore = create < TodoStore >()(( set ) => ({
todos: [],
toggleTodo : ( id ) =>
set (( state ) => ({
todos: state . todos . map (( todo ) =>
todo . id === id ? { ... todo , done: ! todo . done } : todo
),
})),
updateTodoText : ( id , text ) =>
set (( state ) => ({
todos: state . todos . map (( todo ) =>
todo . id === id ? { ... todo , text } : todo
),
})),
}))
Use map() to update items.
type ItemStore = {
items : number []
sortItems : () => void
reverseItems : () => void
}
const useStore = create < ItemStore >()(( set ) => ({
items: [ 3 , 1 , 2 ],
sortItems : () =>
set (( state ) => ({ items: [ ... state . items ]. sort () })),
reverseItems : () =>
set (( state ) => ({ items: [ ... state . items ]. reverse () })),
}))
Methods like sort() and reverse() mutate the array. Always create a copy first with the spread operator.
Immutable Operations Reference
Recommended Immutable Operations
Arrays : [...array], concat(), filter(), slice(), map(), toSpliced(), toSorted(), toReversed()
Objects : { ...object }, Object.assign({}, object)
Mutable Operations to Avoid
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 ,
},
},
},
}))
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'
type State = {
deep : {
nested : {
obj : { count : number }
}
}
increment : () => void
}
const useStore = create < State >()( immer (( set ) => ({
deep: { nested: { obj: { count: 0 } } },
increment : () =>
set (( state ) => {
state . deep . nested . obj . count ++
}),
})))
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