In this lesson, we will add a feature to set the quantity of products added to the cart, as shown below:
Let's get started by enabling the useCartItemsStore
to store the quantity of a product along with its slug. We can transform the cartItems
state into an object with the slug
as the key and the quantity
as the value. Initially, we'll set the value of cartItems
as an empty object. We will replace the toggleIsInCart
function with setSelectedQuantity
, which will update the quantity of a product. If the quantity supplied is zero or negative, the function will remove that product from the cart.
Since we have made cartItems
an object, we should take the length of keys of the object to get the cart items count in Header
component:
Now, let's build a component to manage product quantity updates as shown:
Create a component named ProductQuantity
in the commons directory for the same:
We can retrieve the quantity associated with a specific product and the setSelectedQuantity
function from the Zustand store to display and update the product quantity. We will make use of the paths function from ramda to pick these keys from the cart items store.
Similar to the AddToCart
component, we should prevent navigation to the product page when the user interacts with this component from the ProductListItem
component:
Next, we will update the AddToCart
component to display the ProductQuantity
component mentioned above upon clicking the "Add to cart" button. To achieve this, we will retrieve the selectedQuantity
and setSelectedQuantity
from the cart items store. The AddToCart
component will be displayed if selectedQuantity
is nil; otherwise, the ProductQuantity
component will be displayed. When the user clicks the "Add to cart" button, we will initialize the product quantity for the given slug
to 1
.
Given that we're duplicating the code to retrieve selectedQuantity
and setSelectedQuantity
in both the AddToCart
and ProductQuantity
components, let's extract this logic into a custom hook. Let's create a hook named useSelectedQuantity
under src/components/hooks
for this:
We'll structure the hook to allow the use of the setSelectedQuantity
function without the need to pass the slug
each time. Instead, we'll accept the slug
as an argument to this hook.
Now, you can invoke the useSelectedQuantity
hook from both the AddToCart
and ProductQuantity
components instead of directly using the useCartItemsStore
hook. Additionally, there's no need to pass slug
to the setSelectedQuantity
function.
Now, lets update AddToCart
component.
Similarly, update the ProductQuantity
component as well.
At this stage, upon clicking the "Add to Cart" button, you should be able to view the ProductQuantity
component and subsequently update the quantity as needed.
In the real world, each product has a capped availability, and we must prevent users from increasing the count beyond that limit. To achieve this, we will disable the +
button of the ProductQuantity
when the selected quantity equals the available quantity. For this, the ProductQuantity
component should be aware of the available quantity of the corresponding product.
We will obtain the available quantity for each product from the products array in the ProductList
component. As we pass the entire product details to the ProductListItem
component, we can destructure this value from the ProductListItem
props and pass it down to ProductQuantity
through the AddToCart
component.
We can destructure the available quantity directly from the response in the Product
component:
Pass the availableQuantity
prop to the AddToCart
component from both Product
and ProductListItem
components:
We can then pass the prop down to the ProductQuantity
component:
Utilizing the availableQuantity
prop, we can disable the increment button of the ProductQuantity
component when the selectedQuantity
reaches the availableQuantity
:
To enrich the user experience, it's crucial to offer clear feedback when a button is disabled, providing information on why a certain action cannot be performed. To achieve this, we'll implement a tooltip to be displayed when the increment button is disabled. For this, we can utilize the Tooltip component from neeto-ui:
Typically, the Tooltip component displays the provided content when a user hovers over the element wrapped by it. However, in this case, wrapping the Button
component won't display the content directly because disabled buttons won't trigger any events. To address this, we will enclose the Button
component within a div
element to enable the tooltip functionality.
We'll abstract this functionality into a reusable component called TooltipWrapper
. Instead of directly wrapping the Button
with the tooltip, we can use this component. To ensure the tooltip is displayed only when the button is disabled, we'll introduce a showTooltip
prop for conditional rendering in the TooltipWrapper
.
Now export the TooltipWrapper
component from the commons
folder. Add the following line to the commons/index.js
file.
Now, in the ProductQuantity
component we can show a tooltip by enclosing the +
button with TooltipWrapper
and providing the required props. As we only want the tooltip to appear when the button is disabled, we'll set the showTooltip
prop to isNotValidQuantity
:
Verify if the tooltip is being displayed when the selected quantity equals the available quantity.
To further improve the user experience, instead of displaying the selected quantity as text, we will turn it into an input component. This allows users to directly input the desired quantity without repeatedly clicking the +
button. We will use the Input component from neeto-ui. We will pass the preventNavigation
function as its click handler.
Given that the input component manages its value as a string, we'll also store the quantity as a string in our store. To prevent the removal of items when a user types in a value, we'll implement a condition to check whether the quantity is empty or not. We only need to check the quantity value when the input is not an empty string. We can achieve this using the isNotEmpty function from neetocist
.
To guarantee the proper functionality of the ProductQuantity
component after incorporating the input component, it's crucial to parse the string input to an integer and verify whether the user input is within the specified limits. If not, we'll display a Toastr indicating the available quantity of the product:
We will need to add the ToasterContainer
from react-toastify package for Toastr
component from neetoUI
to work. We have installed the react-toastify
package as a dependency while installing neetoUI
. We only need to import ToastrContainer
and add it in the src/index.jsx
file:
Now, let's define a change handler for the Input
component in the ProductQuantity
component to implement logic to parse and validate user input as discussed above:
We should also ensure that the user doesn't input anything other than digits before updating the quantity in the store. To achieve this, we can define a regex to check whether the input is a digit or an empty string within the constants
file inside the components directory.
In the handleSetCount
function, we can test the input against the above regex and update the store only if the input matches the regex:
To prevent input from the user after entering a quantity outside the limit, we will shift the focus away from the input component by triggering the onBlur
event. This can be accomplished by using the useRef
hook to reference the Input
component.
To ensure that the increment and decrement buttons work as expected after converting the quantity to a string, we should parse an integer from the selectedQuantity
value from the store. This parsed value should be used to increment and decrement the quantity. Otherwise, clicking the button would simply append 1
to the quantity since the +
acts as a concatenation operator for strings. We will also utilize this parsed value to set the isNotValidQuantity
variable, which is used to disable the +
button.
Now, instead of calling the setSelectedQuantity
using selectedQuantity
, we will use the parsedSelectedQuantity
on button clicks:
Great job! You've successfully added the ability to set the quantity of cart items.
Finally, we will store the cart items and quantity details in local storage to persist data after a reload. This ensures that users can view the products added to the cart on their next visit. Zustand provides the persist middleware to store the Zustand state in storage. Let's utilize this middleware to persist the cart items store in local storage:
Now, you can view the details of the cart items even after reloading the page.
Let's commit the new changes:
You can verify the changes here.