In the previous lesson, we saw how we can pass values several levels deep using context without passing them as props through each component in the hierarchy. With context provider, we can wrap a part of the component tree to pass a value to its descendants, and this value can be consumed by any of the descendants of the provider using the useContext hook.
We have used the context in the previous lesson to share the cartItems state between Header and AddToCart component. However, this implementation has a significant drawback. Whenever a state updates, the component containing that state and its children will get re-rendered. Therefore, if we use a state inside the App component the entire application will get re-rendered whenever the state changes. Whereas, we only need to update the UI of Header component and the AddToCart component corresponding to the product added or removed.
In addition to this, when we use context to manage a state, any component that consumes a context will re-render whenever the context value changes. This behaviour might lead to unnecessary re-renders when the state managed by the context API is a complex value. For example, consider that our state is a deeply nested object as shown below:
The above user state is used by two components Teams and Profile, where the Teams component is only concerned with the teams property and the Profile component is concerned with name and contact properties:
Consider that we are updating the contact.address.street path of the user object from the Profile component. In this case, even though the Teams component only uses the teams property of the user object, it will get re-rendered along with the Profile component, which is not a desired behaviour.
Since such unnecessary re-rendering affects the application performance, we use a third-party library called Zustand to efficiently handle state management at BigBinary. Zustand allows us to create a centralized state as stores which can be accessed anywhere in our application, without wrapping components using provider. Zustand also enables components to listen to changes in specific properties and re-render only when that specific property changes.
Let's learn how to use Zustand by replacing the usage of React Context API with Zustand to implement the add-to-cart feature.
First of all, install Zustand by the running the command below:
As specified earlier, states are managed as stores in Zustand. Zustand store acts as a global state that is accessible throughout the application. It returns a hook using which you can access values within the store.
Let's add a store for the cartItems by creating a hook named useCartItemsStore. We keep all our stores in the src/stores directory. Run the below command to create this hook:
Zustand provides the create function to create a store. The create function accepts a callback function as the argument. This callback function should return the initial value of the state.
In our case, we will create our state as an object containing the keys cartItems and toggleIsInCart, where the cartItems will store the slugs of products in the cart and toggleIsInCart will be a function that updates the cartItems array by adding or removing the given slug. We will pass an empty array as the initial value of the cartItems key.
The callback function we passed to the create function receives two functions set and get as the arguments.
The set function is used to update the state in the store. It works similarly to the state setter function but offers the convenience of merging state updates. Instead of explicitly spreading the entire state, we can directly provide the updated key and its value:
The get function retrieves the current state value. In our case, we are solely interested in the set function.
We will utilize the set function to update cartItems within the toggleIsInCart function. Since the set function receives the complete state, we will destructure the cartItems.
The create function returns a hook, which acts as an interface for retrieving values from the store. Using the useCartItemsStore hook, you can easily access the latest cartItems array and the reference to the toggleIsInCart function to update it.
Despite being a hook, useCartItemsStore comes with a set of APIs, allowing us to utilize the store beyond components and hooks. This additional capability is a notable advantage provided by Zustand. Later in this module, we'll see an example demonstrating how to leverage these APIs for external usage.
Now, let's make use of the useCartItemsStore store in our Header component to derive the cart items count.
First, we can remove the useContext hook usage for getting the cart items count:
By calling the useCartItemsStore, we will get the cartItems state, which can be used to derive the cartItemsCount:
However, to truly leverage the performance benefits of Zustand, we should use the store by passing a selector function as the first argument to the hook. The selector function allows you to retrieve only the necessary data from Zustand store. In the case of the Header component, it only needs to be aware of the cart items count. We can achieve this by passing a selector function as shown:
Passing the selector function informs Zustand about what each component is concerned with. When the cart items store updates, Zustand notifies the components using the store. Subsequently, it invokes the selector function to obtain the new value required by the component. It then compares this new value with the previous one using Object.is method and re-renders the component only if there is a change. In the case of the Header component, the component will only re-render when the cartItems.length changes. Therefore, it is essential to use the store with the selector function to extract only the required data from the store, thereby avoiding unnecessary re-renders of components.
Let's substitute the useContext hook with the useCartItemsStore in the AddToCart component as well. The AddToCart component is focused on determining whether a product is in the cart and is also concerned with updating the cartItems array. We can integrate this logic within the selector function:
However, there is one issue with the above implementation. Since the value returned by the selector function is an object generated from the store value, its reference will be different each time the selector function is invoked. As a result, the component will get re-rendered even if the values of isInCart and toggleIsInCart remain the same.
To address this issue, the Zustand store hook also accepts a comparator function, allowing you to specify how to compare the retrieved value from the store. Zustand provides a shallow comparator function, which performs a shallow comparison of the properties or elements of the object or array generated using the selector function.
Let's pass the shallow comparator as the second argument to the useCartItemsStore hook to ensure the component only renders when the reference to isInCart or toggleIsInCart changes:
Now, the AddToCart component will only re-render when the slug corresponding to the component is added or removed from the cartItems state.
Note that we don’t need to pass a shallow comparator unless the value returned from the function is a generated object/array. If we are returning an already existing reference from the store, we don’t need a shallow comparator.
If you need more control over re-rendering, you can pass a custom equality function as a comparator:
Before committing the let's remove the cartItems state and context provider from the App component. We can also remove the CartItemsContext.js file.
Let's commit the new changes:
You can verify the changes here.
One thing to note is that Zustand is not an alternative to React context. Zustand and React context serve different purposes in managing state in React applications. Zustand is a better option when handling complex state updates with a focus on performance. On the other hand, the React context API might be a better choice for managing simple global states, such as themes or current accounts. In some cases, we may even need to use them together to handle states in our application.