Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome back to our course on Zustand.
In our last lesson, we focused on how to read state within your store. We covered using the set(state => ...) updater for simple, atomic updates and the get() function for more complex actions involving conditional logic. We also saw how store.getState() provides non-reactive access to state from outside of React.
Today, we'll address a fundamental requirement of almost any modern web application: implementing asynchronous actions for fetching and managing remote data. You'll learn how to define actions that communicate with an API, handle loading and error states, and update your store with the results. This is an area where Zustand's simplicity really stands out, as it handles async operations natively without requiring middleware like Redux Thunk or Saga.
1. The Core of Asynchronous Actions in Zustand
Handling asynchronous operations in Zustand is surprisingly straightforward. Since actions in a Zustand store are just functions, you can declare them as async functions.
The pattern is simple:
- Define an action as an
asyncfunction. - Inside the function,
awaityour promise-based operation (e.g., afetchcall). - Once the promise resolves, use the
setfunction to update the store with the new data.
Let's start with a very simple example that simulates a network delay.
This video from Cosden Solutions provides a clear, minimal example of an asynchronous action. It demonstrates how an async function can be defined directly within the store.
Please watch from 10:06 to 12:50. The video implements an incrementAsync action using setTimeout to simulate an API call. Notice that no special libraries or middleware are needed; it's just a standard JavaScript async function that calls set when it's done.
As you saw, Zustand doesn't care when you call set. You can call it immediately in a synchronous action or later in a callback or async function. This is the fundamental principle that makes async state management so natural in Zustand.
2. Managing Loading and Error States
In a real-world application, you need to do more than just handle the successful response. You also need to track the loading state (to show a spinner, for example) and any potential errors.
A common and robust pattern is to include loading and error properties in your state alongside your data. Your asynchronous action will then manage these states throughout its lifecycle.
The typical flow looks like this:
- Initiate: The action is called. Immediately,
set({ loading: true, error: null }). - Execute: The API call is made within a
try...catchblock. - Success: If the call succeeds,
set({ data: responseData, loading: false }). - Failure: If the call fails, the
catchblock executes, and youset({ error: errorMessage, loading: false }).
The following article provides a perfect, practical example of this pattern.
The article 'Zustand, When, how and why' on dev.to has an excellent section on handling asynchronous code. It demonstrates the complete loading/error/data pattern.
Please read the section 'Handling Asynchronous Code with Zustand' and the subsection 'Implementing Asynchronous Code'. Focus on the useUserStore example. Observe how the fetchUser action orchestrates the isLoading and error state properties before, during, and after the fetch call.
This pattern is powerful because it makes your UI predictable. Your React components can subscribe to isLoading, error, and user from the store and render the correct interface for each state: a loading indicator, an error message, or the user profile.
The article also includes a UserProfile component that demonstrates exactly how to consume this state, using useEffect to trigger the fetch and conditionally rendering the UI. This should be a familiar pattern from your React experience.
3. Building a Store with Multiple Async Actions
Let's expand on this concept to build a more complete store that handles multiple CRUD-like operations (Create, Read, Update, Delete). This will demonstrate how different async actions can work together to manage a collection of data.
The following resource shows a userStore with actions to fetch, add, and delete users.
React 19: State Management with Zustand
This article from Medium, 'React 19: State Management with Zustand', provides a great example of a store with multiple asynchronous actions. It's a practical demonstration of how you'd manage a list of items fetched from an API.
Please review the code snippet under the heading 'Advanced Pattern: Async Actions with React 19'. Analyze the useUserStore, paying close attention to the implementation of fetchUsers, addUser, and deleteUser. Note how addUser and deleteUser use the functional form of set (set(state => ...)), which we covered in a previous lesson, to correctly update the users array based on the previous state.
This example is particularly instructive because it combines several concepts:
- Async/Await: For handling the API calls.
- Loading/Error State Management: Each action correctly sets the
loadinganderrorflags. - Immutable Updates: The
addUseranddeleteUseractions useset(state => ...)to create a newusersarray, which is essential for React's rendering to work correctly. This reinforces the state update patterns we've already learned.
This userStore is a solid blueprint for managing collections of data from an API in your own applications.
4. The Philosophy of Async in Zustand
As you've seen, Zustand's approach is minimalistic. It doesn't provide a special "async" utility because it doesn't need one. This is a deliberate design choice.
To wrap up, this short clip from Theo - t3.gg succinctly summarizes the philosophy behind Zustand's handling of asynchronous operations.
Watch from 21:56 to 22:33. The key takeaway is that Zustand's approach is simply 'calling set with the result' after your async logic completes, which avoids the complexity of middleware or promise chains seen in other libraries.
This philosophy empowers you to use any promise-based library or pattern you prefer (e.g., axios, fetch, try/catch, .then/.catch) and simply update the state when you're ready.
Conclusion
In this lesson, we've thoroughly covered how to manage remote data in Zustand. You are now equipped to build robust, real-world features that interact with APIs.
Here are the key takeaways:
- Async Actions are Just
asyncFunctions: You can define any action in your store as anasyncfunction. - No Middleware Required: Zustand handles asynchronous operations out of the box, unlike Redux which often requires libraries like Thunk or Saga.
- Manage the Full State Lifecycle: A robust async action should manage
loadinganderrorstates in addition to the data itself, typically using atry...catchblock. - Combine with Existing Patterns: Use the
setfunction (both direct and functional forms) andgetfunction within your async actions just as you would with synchronous ones.
In our next lesson, we will address a common challenge as your application grows: how to organize a large store using the 'slice pattern' for improved scalability and maintainability. This will help you keep your state logic organized and easy to manage, even in complex projects.