Useimperative Handle Hook And Next Js Cart Functionality

·

5 min read

In this tutorial we will be learning about the useImperativeHandle in react, we will be building a simple cart functionality using this hook.

Set up

Create a react project using create react app command Create a folder named Component and in that folder create a file “Cart.tsx”

import { FC, forwardRef, useImperativeHandle, useState } from "react";

interface ICartProps {
  isOpen: boolean;
  toggleCartOpen: () => void;
  ref: any;
}

const Cart:FC<ICartProps> = forwardRef((props, cartRef: any) => {
    const [cart, setCart] = useState([] as any[]);

    useImperativeHandle(cartRef, () => ({
      addToCart(product: any) {
        let newCart = cart;
        let obj = newCart.find((item: any, i: number) => {
          if(item.name === product.name){
            newCart[i] = product;
            setCart(newCart);
            return true;
          }
        })
        if(!obj){
          setCart(newCart.concat(product));
          setTotalAmount(newCart.concat(product).reduce(
            (acc: any, item: any) => acc + item.price * (item.quantity || 1),
            0,
          ));
        }
      },
    }));


    const removeFromCart = (i: number) => {
      let newCart = cart;
      setCart(newCart.filter((item: any, index: number) => index !== i));
    };


    const changeProductQuantity = (i: number, quantity: number) => {
      let newCart = cart;
      newCart[i].quantity = quantity;
      setCart(newCart);

    }

    return (
        <div
          className="flex flex-col justify-between w-20 h-10 fixed bottom-8 right-16"
        >
            <div>
            {cart.map((item: any, i: any) => (
            <div
                className="flex flex-col w-[95%] py-1 border-b border-solid border-gray-800"
                key={i}
            >
                <div className="flex flex-row justify-between">
                    <span
                        className="text-black text-base"
                    >
                        {item.name.toUpperCase()}
                    </span>

                    <button
                        className="py-1 px-2 text-center text-red-600 cursor-pointer"
                        onClick={() => removeFromCart(i)}
                    >
                        X
                    </button>
                </div>

                <div className="flex flex-row justify-between">
                    <p
                        className="text-gray-800 m-0"
                    >    
                        N{item.price} x 
                    </p>
                    <input
                        type="number"
                        className="w-[20%] py-1 px-2 text-center bg-gray-200 rounded-md"
                        defaultValue={item.quantity || 1}
                        onChange={(e: any) => onProductQuantityChange(i, e.target.value)}
                    />

                </div>
            </div>
            ))}


        </div>            
        </div>
    )
});

export default Cart

In the above file, we first define an interface for our component which declares the props the component expects and their types.

Note that we are wrapping our component with a forwardRef, this is another react hook that lets us pass a ref from a parent component to a child component, this is necessary to get the useImperativeHandle hook to work.

We then declare a state to store the cart data.

The idea with this implementation is, we declare the state that holds the cart data inside the Cart.tsx component and also the functions for adding, removing from the cart, and changing the number of products in the cart. But we want to call the addToCart function from outside this component (eg where the products data will be available), we want to be able to call only this function from outside the component, that’s why we wrap it in the useImperativeHandle function. The useImperativeHandle takes a ref, which will be passed in from the parent component with the help of the forwardRef handle, and returns an object (eg our addToCart function). The addToCart function checks if the product is already in the cart, if not, it adds it to the cart state.

The other functions are used to remove items from the cart and change the no of items in the cart.

Let us create a ProductComponent file in the Components folder Paste the following code in the file

import Image from 'next/image
import { FC } from 'react'
import { HiShoppingCart } from 'react-icons/hi'

interface IProductComponentProps {
    key: any;
    result: any;
    addToCart: (product: any) => void;
}

const ProductComponent: FC<IProductComponentProps> = (props) => {

    return (
        <div
            className='flex flex-col bg-white rounded-md w-[70%] md:w-[90%] lg:w-[100%] mx-auto mb-8 
             p-4 lg:flex-row'
        >
            <Image 
                src={props.result.image}
                alt="product image"
                width={100}
                height={200}
                className='rounded-md justify-center mr-4 h-auto cursor-pointer'
            />

            <div 
                className="flex flex-col justify-between ml-3 pt-2"
            >
                <h3 
                    className='text-lg font-mono'
                >
                    {props.result.name}
                </h3>
                <p 
                    className='text-gray-500 text-base line-clamp-2 mb-2'
                >
                    {props.result.description}
                </p>
                <div 
                    className='flex flex-row justify-between mb-2'
                >
                    <p 
                        className='text-base text-green-700 font-semibold'
                    >
                        {props.result.price }
                    </p>

                </div>

                <button
                    className="bg-orange-500 hover:bg-orange-700 text-white font-bold py-2 px-2 w-[50%] 
                    flex justify-center mx-auto rounded-md"
                    onClick={() => props.addToCart(props.result)}
                >
                    <HiShoppingCart className="text-lg text-center" />
                </button>
            </div>
        </div>
    )
}

export default ProductComponent

In the above file, we are declaring the interface for the ProductComponent props, we then use those props to create the component.

Now, let us create a Results.tsx file in our component folder. in our Results.tsx file, we will import the Cart.tsx component and the product component, this is where we trigger the useImperativeHandle hook.

import { useRef, useState } from "react";
import Cart from "../Components/Cart";

const results = () => {
    const cartRef = useRef({});

    const addToCart = (newProduct:any) => {
        cartRef.current?.addToCart(newProduct);
    }

    const [cartOpen, setCartOpen] = useState(false);

 return (
        <div>
        <Cart 
                isOpen={cartOpen}
                toggleCartOpen={openOrCloseCart}
                ref={cartRef}
            />

      </div>

}

Let’s go over things as it stands right now, in the above file, we are defining a ref named “cartRef”, this is the ref that is passed as props to the cart component in order to trigger the useImperativeHandle in the cart component. Note that the addToCart function we are defining in this file triggers the addToCart function in the Cart component, which is returned by the useImperativeHandle, by calling the current method on the ref we are passing to the component.

Let’s pass in some dummy data and display the ProductComponent

import { useRef, useState } from "react";
import Cart from "../Components/Cart";

const results = () => {
    const cartRef = useRef({});

    const addToCart = (newProduct:any) => {
        cartRef.current?.addToCart(newProduct);
    }

    const [cartOpen, setCartOpen] = useState(false);

     const products = [
    {
      name: 'SHIRT',
      description: 'Product Description',
      image: 'https://via.placeholder.com/100',
      price: '$100',
      discount_rate: '10%',
      quantity: 2,
    },
    {
      name: 'Attack on Titan',
      description: 'Manga',
      image: 'https://via.placeholder.com/100',
      price: '$100',
      discount_rate: '10%',
    quantity : 2
    },
    {
      name: 'Demon slayer ',
      description: 'Manga',
      image: 'https://via.placeholder.com/100',
      price: '$100',
      discount_rate: '10%',
      quantity : 1
    },
    ]



 return (
        <div>
        <Cart 
                isOpen={cartOpen}
                toggleCartOpen={openOrCloseCart}
                ref={cartRef}
            />

        {
          products.map((product:any) => (
            <ProductComponent 
              key={product.name}
              result={product} 
              addToCart={addToCart}
            />
          ))
        }
      </div>
}