Featured resource
pluralsight AI Skills Report 2025
AI Skills Report

As AI adoption accelerates, teams risk of falling behind. Discover the biggest skill gaps and identify tools you need to know now.

Learn more
  • Labs icon Lab
  • Core Tech
Labs

Guided: State Management with Zustand

This Guided Code Lab will teach you how to manage state in React applications using Zustand. By building a functional book collection application, you'll learn to create, manipulate, and persist state with Zustand.

Labs

Path Info

Level
Clock icon Beginner
Duration
Clock icon 1h 19m
Published
Clock icon Oct 05, 2024

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Table of Contents

  1. Challenge

    Introduction

    Welcome to the lab Guided: State Management with Zustand.

    Zustand is a small, fast, and flexible state management library for React. It's designed to be easy to use while providing powerful features for managing global state in your applications. Zustand is unopinionated, meaning it doesn't enforce a particular pattern or structure, allowing you to choose the best approach for your needs.

    In this lab, you'll gain hands-on experience with Zustand by building a practical book collection application with features like adding books, updating library details, clearing books, logging books, and persisting data. You'll learn how to create stores, manipulate state, work with nested objects, combine slices, and use middleware to persist state. ---

    Familiarizing with the Program Structure

    The application includes the following files:

    • App.js: The main React component that brings together other components, such as LibraryDetails and BookList.
    • store.js: The JavaScript file where the Zustand store is defined.
    • AddBook.js: A React component that allows users to add new books to the collection.
    • BookList.js: A React component that displays the list of books.
    • LibraryDetails.js: A React component that allows users to view and update the library details.

    You will focus on working with the store.js file to gradually implement the necessary functionality.

    Start by examining the provided code to understand the program structure. Once you're ready, begin coding. If you run into any issues, a solution directory is available for reference. It contains subdirectories named step2, step3, and so on, each corresponding to a step in the lab. Within each subdirectory, solution files follow the naming convention [file]-[step number]-[task number].js (e.g., store-2-1.js in step2), where [file] represents the file name, the first number indicates the step, and the second represents the task.

  2. Challenge

    Creating a Store

    To create a store with Zustand, you use the create function. This function initializes the store with an initial state and optionally includes actions for updating the state.

    Here's a basic example of how to create a store:

    import { create } from 'zustand';
    
    // Create a store with an initial state
    const useStore = create((set) => ({
      dogs: 0, // Initial state
      addDog: () => set((state) => ({ dogs: state.dogs + 1 })), // Action to update state
    }));
    

    This example shows a store with an initial state containing a dogs property set to 0. It also defines an action addDog to update the state by incrementing the bears property.

    Once you have created a store, you can fetch the entire state, but be careful as the following will cause the component to update on every state change:

    const state = useStore();
    

    To make your components more efficient, you can select specific slices of the state. Zustand detects changes with strict-equality (old === new) by default, which is efficient for atomic state picks:

    const dogs = useStore((state) => state.dogs);
    const addDog = useStore((state) => state.addDog);
    

    This example selects the dogs state and the addDog action separately. This ensures that the component only re-renders if the dogs state or the addDog action changes.

    After creating the store and selecting the state slices, you can use them in your React components. For example:

    const DogCounter = () => {
      const dogs = useStore((state) => state.dogs);
      const addDog = useStore((state) => state.addDog);
    
      return (
        <div>
          <h1>{dogs} dogs around here ...</h1>
          <button onClick={addDog}>Add a dog</button>
        </div>
      );
    };
    

    This component accesses the dogs state and the addDog action from the store and use them to display the number of dogs and add a new dog when the button is clicked.

    In the next tasks, you'll start implementing a store for managing books and use it in your components.

  3. Challenge

    Working with State

    To manipulate state in your Zustand store, you can use the set and get methods.

    The set Method

    The set method is used to update the state in the store. It accepts a function that receives the current state and returns the new state. Zustand will then merge the returned state with the existing state.

    Here's an example of how to use this method:

    const useStore = create((set) => ({
      dogs: 0, // Initial state
      addDog: () => set((state) => ({ dogs: state.dogs + 1 })), // Action to update state
    }));
    

    In this example, the addDog action uses the set method to increment the dogs property in the state.

    By default, Zustand performs shallow merging when updating the state. However, you can disable this behavior by passing true as the second argument to the set method. When this is done, Zustand will replace the entire state with the new state returned by the function.

    Default Behavior (Shallow Merging):

    // The "dogs" property is merged with the other properties in the state
    set((state) => ({ dogs: state.dogs + 1 }));
    

    Replacing Entire State:

    // The state now consists of only the "dogs" property
    set((state) => ({ dogs: state.dogs + 1 }), true);
    

    In most cases, the default shallow merging behavior is preferred, as it simplifies state updates and reduces the risk of accidentally overwriting the entire state.

    The get Method

    The get method is used to retrieve the current state from the store. This can be useful when you need to perform actions that depend on the existing state.

    Here's an example of how to use the get method:

    const useStore = create((set, get) => ({
      dogs: 0, // Initial state
      addDog: () => set((state) => ({ dogs: state.dogs + 1 })),
      logDogs: () => {
        console.log(get().dogs); // Retrieve the current state
      },
    }));
    

    In this example, the logDogs action uses the get method to log the current number of dogs to the console.

    You're ready to implement actions in the Zustand store to manage the state of books in your application. You will start with the addBook action. This action should use the set function to allow users to add books to the store. It's time for the updateBook action. This action should use the set function to update a given book in the store. You will now add the clearBooks action. This action should use the set function to reset the books array. Now you can implement the logBooks action. This action should use the get function to log the books array to the console.

  4. Challenge

    Using Nested Objects

    When dealing with nested objects in state management, one common issue is how to update a deeply nested property without overwriting the entire object. For example, consider the following state structure:

    const useStore = create((set) => ({
      user: {
        profile: {
          name: 'John Doe',
          age: 30,
        },
        settings: {
          theme: 'dark',
          notifications: true,
        },
      },
    }));
    

    If you want to update the user's name, you need to ensure that the rest of the user object remains unchanged. A naive update might overwrite other parts of the user object.

    Zustand allows you to update nested objects by merging the new state with the existing state. This is achieved by using the set function and spreading the current state to retain unchanged properties.

    Here's how you can update the user's name while keeping the rest of the user object intact:

    const useStore = create((set) => ({
      user: {
        profile: {
          name: 'John Doe',
          age: 30,
        },
        settings: {
          theme: 'dark',
          notifications: true,
        },
      },
    
      // Action to update the user's name
      updateUserName: (newName) => set((state) => ({
        user: {
          ...state.user, // Keep other parts of the user state unchanged
          profile: {
            ...state.user.profile, // Keep other parts of the profile unchanged
            name: newName, // Update the name property
          },
        },
      })),
    }));
    

    In this example, the updateUserName action updates the name property within the profile object. Notice how the rest of the user object, including settings, remains unchanged. This is achieved by spreading the state.user and state.user.profile objects.

    In summary:

    • Use the spread operator (...) to merge the new state with the existing state, ensuring that unchanged properties are preserved.
    • Zustand performs shallow merging by default, so when updating nested objects, you need to manually merge the nested properties.
    • Zustand's approach allows you to update deeply nested properties without overwriting the entire state.

    Now you're ready to implement a store that includes nested objects and actions to update them.

  5. Challenge

    The Slices Pattern

    While having one store is often recommended for simplicity, there are cases where dividing your store into smaller, modular slices can be beneficial, especially as your application grows in complexity.

    The slices pattern involves breaking down a large store into smaller, more manageable slices. Each slice is responsible for a specific part of the state and its associated actions. This approach promotes modularity and makes the state management easier to maintain and scale:

    1. Modularity: By dividing the state into smaller slices, each slice can focus on a specific aspect of the application. This makes the code more modular and easier to understand.
    2. Maintainability: Smaller slices are easier to maintain and update. Changes to one slice are less likely to impact other slices.
    3. Scalability: As your application grows, managing a single large store can become cumbersome. Slices allow you to scale your state management more effectively.

    To create a slice, define a function that takes set (and optionally get) as arguments and returns an object with the initial state and actions for that slice. Here's an example of two slices:

    // Slice for managing dogs-related state and actions.
    const dogsSlice = (set, get) => ({
      dogs: [], // Initial state: an empty array of dogs.
    
      // Action to add a new dog to the dogs array.
      addDog: (dog) => set((state) => ({
        dogs: [...state.dogs, dog],
      })),
    
      // Action to clear all dogs.
      clearDogs: () => set({ dogs: [] }),
    });
    
    // Slice for managing cats-related state and actions.
    const catsSlice = (set) => ({
      cats: [], // Initial state: an empty array of cats.
    
      // Action to add a new cat to the cats array.
      addCat: (cat) => set((state) => ({
        cats: [...state.cats, cat],
      })),
    
      // Action to clear all cats.
      clearCats: () => set({ cats: [] }),
    });
    

    After defining the slices, you can combine them into a single store using the create function. Spread the slices into the store definition to include their state and actions:

    import { create } from 'zustand';
    
    // Creating the Zustand store that combines both slices (dogs and cats).
    const useStore = create((set, get) => ({
      ...dogsSlice(set, get), // Include the dogs slice in the store.
      ...catsSlice(set), // Include the cats slice in the store.
    }));
    

    In this example, both the dogs and cats slices are combined into a single store. The combined store includes the state and actions from both slices, allowing you to manage the dogs and cats state separately, but within the same store.

    You're now ready to implement the slices pattern in your application.

  6. Challenge

    Using Middleware

    Middleware in Zustand is a way to extend the functionality of your stores. It allows you to intercept and modify actions and state updates, adding additional behavior before or after the state changes. Middleware can be used for various purposes, such as:

    1. Logging: Logs every state change to the console, which is useful for debugging and understanding how state changes over time.
    2. Persistence: Saves the state to local storage or another storage mechanism, allowing it to persist across sessions.
    3. Integration with other libraries: For example, you can use the Immer middleware to make updating deeply nested objects more convenient.

    One of the most commonly used middleware in Zustand is the persist middleware. It allows you to save the state of your store to a storage mechanism, such as localStorage or sessionStorage, and automatically restore the state when the application is reloaded.

    To use the persist middleware, you need to wrap your store definition with the persist function. The persist function takes two arguments:

    1. The Store Definition: A function that defines the initial state and actions of your store.
    2. The Configuration Object: An object that specifies how the state should be persisted.

    Here's an example of how to use the persist middleware:

    import { create } from 'zustand';
    import { persist } from 'zustand/middleware';
    
    // Define the store
    const useStore = create(
      persist(
        (set, get) => ({
          // Initial state and actions here
          items: [],
          addItem: (item) => set((state) => ({
            items: [...state.items, item],
          })),
        }),
        {
          name: 'my-storage', // The key used to save the state in localStorage
          getStorage: () => localStorage, // Optional, by default, 'localStorage' is used
        }
      )
    );
    

    In this example:

    • The store is defined with an initial state (items) and an action (addItem).
    • The persist function wraps the store definition.
    • The configuration object specifies the key (name) used to save the state in localStorage and the storage mechanism (getStorage).

    You're now ready to implement the persist middleware in your application.

  7. Challenge

    Conclusion

    Congratulations on successfully completing this Code Lab!

    To run the application, either click the Run button in the bottom-right corner of the screen or in a Terminal tab execute:

    npm start
    

    Then, click the following link to open the application in a new browser tab: {{localhost:3000}}. Alternatively, you can refresh the Web Browser tab to reload and view the application there.

    This lab covers just the fundamentals of Zustand. For more information, read the Zustand documentation and guides. ---

    Extending the Program

    Consider exploring these ideas to further enhance your skills and expand the capabilities of the program:

    1. Dark/Light Mode Toggle: Add a dark/light mode toggle to the application. Manage the theme state in the Zustand store.

    2. Filtering and Sorting: Add filtering and sorting functionalities to the book list. Allow users to filter books by author or title and sort them by different criteria (e.g., title, author).

    3. Search Functionality: Implement a search feature that allows users to search for books by title or author. Update the state to reflect the search results.

    4. Pagination for Book List: Implement pagination for the book list. Add controls to navigate between different pages of books and update the state management logic to load a specific page of books based on the selected page number. ---

    Related Courses on Pluralsight's Library

    If you're interested in further honing your React skills or exploring more topics, Pluralsight offers several excellent courses in the following path:

    These courses cover many aspects of React programming. Check them out to continue your learning journey in React!

Esteban Herrera has more than twelve years of experience in the software development industry. Having worked in many roles and projects, he has found his passion in programming with Java and JavaScript. Nowadays, he spends all his time learning new things, writing articles, teaching programming, and enjoying his kids.

What's a lab?

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Provided environment for hands-on practice

We will provide the credentials and environment necessary for you to practice right within your browser.

Guided walkthrough

Follow along with the author’s guided walkthrough and build something new in your provided environment!

Did you know?

On average, you retain 75% more of your learning if you get time for practice.