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="https://via.placeholder.com/100"
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>
}
Top comments (0)