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 ofcreate
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. Thewishlist
property is aSet
, and following React’s best practices, we should create a newSet
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.