Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Replacing vs. Merging State with `set`

Hello! Welcome to your next lesson on Zustand.

In our previous lessons, we covered the fundamentals of Zustand. You learned how to set up a store with create, define state and actions, and connect it to your React components using the generated hook.

Today, we'll dive deeper into the most critical function for state updates: set. The goal of this lesson is to differentiate between replacing state and merging state when using the set function. Understanding this distinction is fundamental to correctly managing state transitions, avoiding common bugs, and writing efficient update logic.

Let's get started.

1. The Default Behavior: Shallow Merging

When you call the set function with an object, Zustand doesn't replace your entire state. Instead, it performs a shallow merge. This means it merges the properties of the object you provide into the existing state at the top level. Any top-level properties you don't specify in your update remain untouched.

This is a convenient default that differs from React's useState hook, where you are responsible for manually merging the old and new state for objects.

To see this in action, let's watch a segment from a tutorial that explains this merging behavior.

React State Management with Zustand & TypeScript

This video from Anson's channel provides a clear explanation of how the set function merges state by default, contrasting it with the behavior of React's useState hook.

Please watch from 06:39 to 12:01. Pay close attention to how the set function is used to update only a specific field (username) without affecting other fields (email) in the state object.

As the video demonstrates, when you call set({ username: 'new name' }), the email field in the store is preserved. Zustand handles the merge for you: Object.assign(currentState, newState).

This is powerful, but the key word is shallow. What happens when your state is nested?

2. The Challenge with Nested State

Shallow merging only applies to the first level of your state object. If you update a property within a nested object, you must handle the immutability of the parent objects yourself to avoid losing data.

Let's look at an example. Imagine your store looks like this:

const useCatStore = create(set => ({
  cats: {
    bigCats: 0,
    smallCats: 0
  },
  increaseBigCats: () => {
    // Incorrect update! This replaces the entire 'cats' object.
    set({ cats: { bigCats: 1 } }); 
    // The 'smallCats' property is now gone.
  }
}));

To correctly update a nested value, you must copy the existing state at each level you are not modifying. The spread syntax (...) is perfect for this.

The correct implementation would be:

const useCatStore = create(set => ({
  cats: {
    bigCats: 0,
    smallCats: 0
  },
  increaseBigCats: () => set(state => ({
    cats: {
      ...state.cats, // <-- Spread the existing 'cats' object
      bigCats: state.cats.bigCats + 1 // <-- Then, overwrite the property you want to change
    }
  }))
}));

The following video provides a practical demonstration of this exact scenario.

React Zustand Tutorial - 2. get(), set() and the immer middleware (typescript)

This video from Conny Gu clearly illustrates the difference between how set handles first-level properties versus nested objects.

Watch the segment from 00:18 to 05:57. The presenter builds a catStore with a nested cats object and shows why the spread operator is necessary for updating it correctly.

This pattern of spreading nested objects is a cornerstone of immutable updates in JavaScript, and it's essential when working with Zustand's default merge behavior.

3. The Alternative: Replacing State

While merging is the default, Zustand provides a way to completely replace the state. This is useful in specific scenarios, such as when your state is a primitive value (e.g., a single number or string) or when you need to reset the entire store.

To replace the state, you pass true as the second argument to the set function: set(newState, true).

The official Zustand documentation provides a concise explanation and example.

create ⚛️ - Zustand

Let's look at the official Zustand documentation on the create API. It explicitly describes the replace parameter.

Please read the section titled 'Updating Primitives in State'. Focus on the note that explains the replace parameter and look at how useXStore.setState(nextX, true) is used in the code example.

When you use set(newState, true), any properties from the old state that are not in newState will be discarded.

Summary: Merge vs. Replace

Let's summarize the key differences in a table.

Behavior set(partialState) (Default Merge) set(newState, true) (Replace)
How it works Shallowly merges partialState with the current state. It's like Object.assign(currentState, partialState). Completely replaces the entire state object with newState.
Use Case The most common approach. Used for updating one or more top-level properties of your state object. Less common. Used for resetting state, or when the entire state is a primitive value (number, string, etc.).
Example set({ bears: state.bears + 1 }) set({ bears: 0, fishes: 0 }, true)
Key Consideration For nested objects, you must manually spread parent levels to preserve their data. All properties from the old state that are not included in newState are lost.

A Note on Arrays

Updating arrays in your state follows the same principles of immutability. While the top-level property holding the array is subject to the merge/replace behavior of set, the crucial part is that you must create a new array for the update, rather than mutating the existing one.

Common immutable array operations include:

  • Adding an item: set(state => ({ posts: [...state.posts, newPost] }))
  • Removing an item: set(state => ({ posts: state.posts.filter(post => post.id !== idToRemove) }))

The video from Anson we watched earlier has excellent follow-up segments demonstrating these patterns. If you'd like to review, you can watch his implementation for adding a post (starting at 40:38) and removing a post (starting at 51:33) in the "React State Management with Zustand & TypeScript" video.

As you can see, understanding how to work with immutable data structures is central to using Zustand effectively.

Conclusion

In this lesson, we've dissected the two primary modes of Zustand's set function:

  • Shallow Merging (Default): set(partialState) is the standard, convenient way to update top-level state properties. For nested state, you must manually preserve data by spreading parent levels.
  • State Replacement: set(newState, true) offers a way to completely overwrite the state, which is useful for resets or when dealing with primitive state values.

Mastering this distinction will give you precise control over your state transitions.

In our next lesson, we will explore how to access the current state from within an action using the get function. This is particularly useful for complex state transitions where the next state depends on multiple values from the current state, building directly on what we've learned today.

Can't find a good explanation? Sign up and we'll make it for you

Sign up