How to Use Zustand in Your React Projects

How to Use Zustand in Your React Projects

·

5 min read

Zustand has recently emerged as a rising star in the world of state management tools, known for its simplicity and scalibilty. In this article, we will learn how to use Zustand in React projects and see why it is simpler compared to other state management libraries like Redux.

1. Introduction to Zustand

Zustand is a small, fast and scalable state management solution using Flux principles. It is created by Jared Palmer and Daishi Kat, who is also the author of Jotai—another state management library. You can read more about the story behind of Zustand in Daishi’s blog: How Zustand Was Born.

Zustand’s simplicity is evident in both its set up and usage. Unlike Redux, it does not require action types, action creators, reducers or being wrapped by context provider like Context API. In the following sections, we will explore how to use Zustand by building a simple application for customer profile management.

2. Creating first Store with Zustand

First, like any other npm package, we need to install the Zustand library in out project:

npm i zustand

In this example, we will use TypeScript to create a store for managing customer profile with Zustand. To begin, let’s define types for our store:

type CustomerStoreState = {
  customer: {
    name: string;
    gender: string;
    address: {
      city: string;
      country: string;
    };
    wishlist: Set<string>;
  };
};
type CustomerStoreActions = {
  updateAddress: (address: CustomerStoreState["customer"]["address"]) => void;
  addToWishlist: (item: string) => void;
};
type CustomerStore = CustomerStoreState & CustomerStoreActions;

As we can see, our store include a customer state, which is an object representing the customer profile, along with two methods to update this profile. Next, we will create customerStore using the create API from Zustand:

import { create } from "zustand";

const customerStore = create<CustomerStore>()((set) => ({
  customer: {
    name: "Alice",
    gender: "Female",
    address: {
      city: "New York",
      country: "USA",
    },
    wishlist: new Set(["Shoes", "Wallet"]),
  },
  updateAddress: (address) => {
    set((state) => ({
      ...state,
      customer: {
        ...state.customer,
        address,
      },
    }));
  },
  addToWishlist: (item) => {
    set((state) => ({
      ...state,
      customer: {
        ...state.customer,
        wishlist: new Set(state.customer.wishlist).add(item),
      },
    }));
  },
}));
export default customerStore;

In the code above:

  • The set function inside the callback of create allows us to update the state we defined.

  • The updateAddress method is used to update the customer’s address. Since this property is a nested object, and following best practices from Zustand documentation, we should treat it as an immutable object, similar to Redux.

  • The addToWishlist method is used to add an item to the customer's wishlist. The wishlist property is a Set, and following React’s best practices, we should create a new Set when updating it.

3. Using Zustand in components

Alright, after creating our customer store, we will now bind it to our component:

import { useState } from 'react';
import useCustomerStore from './stores/customerStore';

const App = () => {
  const { customer, updateAddress, addToWishlist } = useCustomerStore();

  const [city, setCity] = useState(customer.address.city);
  const [country, setCountry] = useState(customer.address.country);
  const [wishlistItem, setWishlistItem] = useState("");

  const handleAddressUpdate = () => {
    updateAddress({ city, country });
  };

  const handleAddToWishlist = () => {
    if (wishlistItem.trim()) {
      addToWishlist(wishlistItem.trim());
      setWishlistItem("");
    }
  };

  return (
    <div>
      <h2>Customer Profile</h2>
      <p><strong>Name:</strong> {customer.name}</p>
      <p><strong>Gender:</strong> {customer.gender}</p>
      <p><strong>City:</strong> {customer.address.city}</p>
      <p><strong>Country:</strong> {customer.address.country}</p>
      <div>
        <h3>Address</h3>
        <input
          type="text"
          value={city}
          onChange={(e) => setCity(e.target.value)}
          placeholder="City"
        />
        <input
          type="text"
          value={country}
          onChange={(e) => setCountry(e.target.value)}
          placeholder="Country"
        />
        <button onClick={handleAddressUpdate}>Update Address</button>
      </div>
      <div>
        <h3>Wishlist</h3>
        <ul>
          {[...customer.wishlist].map((item, index) => (
            <li key={index}>{item}</li>
          ))}
        </ul>
        <input
          type="text"
          value={wishlistItem}
          onChange={(e) => setWishlistItem(e.target.value)}
          placeholder="Add to Wishlist"
        />
        <button onClick={handleAddToWishlist}>Add Item</button>
      </div>
    </div>
  );
};

export default App

That’s it! Pretty straightforward, right? All we need to do is create a store and then retrieving state values and methods wherever needed. It works similarly to custom hooks—no configuration and no wrappers required. This is very handy compared to Redux or Context API.

However, there is one important detail to consider: how we retrieve state from customer store. In the example above, we use this approach (let’s call it first approach):

  const { customer, updateAddress, addToWishlist } = useCustomerStore();

This way works fine when we only have only one component (like App) consuming this store. However, in practice, it’s better to retrieve state values using this alternative syntax (let’s call it second approach):

  const customer = useCustomerStore((state) => state.customer);
  const updateAddress = useCustomerStore((state) => state.updateAddress);
  const addToWishlist = useCustomerStore((state) => state.addToWishlist);

The difference is that the first approach retrieves everything from the store, meaning any state update—even if it’s unrelated to the component—will cause a re-render. Otherwise, the second approach is more efficient because it picks only necessary state values. This ensures the component re-renders only when the specific values it depends on change, improving performance.

4. Middlewares in Zustand

Let’s revisit the updateAddress method. When updating an nested object in state, we need to maintain its immutability by copying each nested properties. There is a way to do it more convenientlym which is using immune middleware—it should be familiar to those who has worked with Redux.

To use middleware in Zustand, we need to wrap the state creator function with the middleware, immer helps manage the state in an immutable way without manually copying every nested property. Here is an example of how we can use it:

const customerStore = create<CustomerStore>()(
  immer((set) => ({
    customer: {
      name: "Alice",
      gender: "Female",
      address: {
        city: "New York",
        country: "USA",
      },
      wishlist: new Set(["Shoes", "Wallet"]),
    },
    updateAddress: (address) =>
      set((state) => {
        state.customer.address = address;
      }),
    addToWishlist: (item) => {
      set((state) => {
        state.customer.wishlist.add(item);
      });
    },
  }))
);

Another useful middleware in Zustand is the persist middleware. It enables us to persist a store's state across page reloads or application restarts by storing the state in local storage. This is specially handy for states that need to be persistent, such as user preferences like language or theme settings. For example:

import { create } from "zustand";
import { persist } from "zustand/middleware";

type LanguageStore = {
  language: string;
  updateLanguage: (newLanguage: string) => void;
};

const useLanguageStore = create<LanguageStore>()(
  persist(
    (set) => ({
      language: "english",
      updateLanguage: (newLanguage: string) => {
        set({ language: newLanguage });
      },
    }),
    {
      name: "language-storage",
    }
  )
);

5. Conclusion

I hope with these examples of Zustand usage have helped you understand why this library become popular in such a shortime and why it’s prefered by many developers. These examples cover just the basic usage of Zustand, but there’s much more to explore. You can dive deeper into the API and other middlewares by visiting the official documentation here: Zustand Documentation.