Usage of ref and useRef in parent and child components in reactjs project, its giving the default value only

60 views Asked by At

My project is about displaying products and to cart and then checkout. I am attaching below code snippets for three components which are: search (home page to display products), product component and cart.

Now the products are rendering properly, but when I click on add to cart button of product, it's not rendering anything and cartRef.current is logging:

cartRef:  {current: null}
Search.jsx:210 cartRef.current:  null

Could anyone please help me to understand where I am going wrong? In my opinion, I think it's related to using ref in Cart component.

Search.jsx:

import { Input, message } from "antd";
import { useState, useEffect, useRef } from "react";
import { useNavigate } from "react-router-dom";
import { config } from "../../App";
import Cart from "../Cart/Cart";
import Header from "../Header/Header";
import Product from "../Product/Product";
import { Row, Col } from "antd";
import Footer from "../Footer/Footer";
import "./Search.css";
   
const Search = () => {
    const navigate = useNavigate();
    const cartRef = useRef(null);

    const [loading, setLoading] = useState(false);
    const [loggedIn, setLoggedIn] = useState(false);
    const [filteredProducts, setFilteredProducts] = useState([]);
    const [products, setProducts] = useState([]);
    const [debounceTimeout, setDebounceTimeout] = useState(0);


    /**
   * Check the response of the API call to be valid and handle any failures along the way
   *
   * @param {boolean} errored
   *    Represents whether an error occurred in the process of making the API call itself
   * @param {Product[]|{ success: boolean, message: string }} response
   *    The response JSON object which may contain further success or error messages
   * @returns {boolean}
   *    Whether validation has passed or not
   *
   * If the API call itself encounters an error, errored flag will be true.
   * If the backend returns an error, then success field will be false and message field will have a string with error details to be displayed.
   * When there is an error in the API call itself, display a generic error message and return false.
   * When there is an error returned by backend, display the given message field and return false.
   * When there is no error and API call is successful, return true.
   */

    const validateResponse = (errored, response) => {
        if (errored || (!response.length && !response.message)) {
            message.error(
                "Error: Could not fetch products. Please try again!"
            );
            return false;
        }

        if (!response.length) {
            message.error(response.message || "No products found in the database");
            return false;
        }

        return true;
    };

    /**
   * Perform the API call over the network and return the response
   *
   * @returns {Product[]|undefined}
   *    The response JSON object
   *
   * -    Set the loading state variable to true
   * -    Perform the API call via a fetch call: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
   * -    The call must be made asynchronously using Promises or async/await
   * -    The call must handle any errors thrown from the fetch call
   * -    Parse the result as JSON
   * -    Set the loading state variable to false once the call has completed
   * -    Call the validateResponse(errored, response) function defined previously
   * -    If response passes validation, return the response object
   */
    const performAPICall = async () => {
        let response = {};
        let errored = false;

        setLoading(true);

        try {
            response = await (await fetch(`${config.endpoint}/products`)).json();
        } catch (e) {
            errored = true;
        }

        setLoading(false);

        if (validateResponse(errored, response)) {
            return response;
        }
    };

    /**
 * Definition for debounce handler
 * This is the function that is called whenever the user types or changes the text in the searchbar field
 * We need to make sure that the search handler isn't constantly called for every key press, so we debounce the logic
 * i.e. we make sure that only after a specific amount of time passes after the final keypress (with no other keypress event happening in between), we run the required function
 *
 * @param {{ target: { value: string } }} event
 *    JS event object emitted from the search input field
 *
 * -    Obtain the search query text from the JS event object
 * -    If the debounceTimeout class property is already set, use clearTimeout to remove the timer from memory: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/clearTimeout
 * -    Call setTimeout to start a new timer that calls below defined search() method after 300ms and store the return value in the debounceTimeout class property: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout
 */

    const debounceSearch = (event) => {
        const value = event.target.value;

        if (debounceTimeout) {
            clearTimeout(debounceTimeout);
        }

        setDebounceTimeout(
            setTimeout(() => {
                search(value);
            }, 300)
        );
    };

    const search = (text) => {
        setFilteredProducts(
            products.filter(
                (product) =>
                    product.name.toUpperCase().includes(text.toUpperCase()) ||
                    product.category.toUpperCase().includes(text.toUpperCase())
            )
        );
    };

    const getProducts = async () => {
        const response = await performAPICall();

        if (response) {
            setProducts(response);
            setFilteredProducts(response.slice());
        }
    };

    useEffect(() => {
        getProducts();

        if (localStorage.getItem("email") && localStorage.getItem("token")) {
            setLoggedIn(true);
        }
    }, []);

    const getProductElement = (product) => {
        return (
            <Col xs={24} sm={12} xl={6} key={product._id}>
                <Product
                    product={product}
                    addToCart={() => {
                        if (loggedIn) {
                            console.log('cartRef: ', cartRef);
                            console.log('cartRef.current: ', cartRef.current)
                            cartRef && cartRef.current && cartRef.current.postToCart(product._id, 1, true);
                        } else {
                            navigate("/login");
                        }
                    }}
                />
            </Col>
        );
    };

    return (
        <>
            {/* Display Header with Search bar */}
            <Header>
                <Input.Search
                    placeholder="Search"
                    onSearch={search}
                    onChange={debounceSearch}
                    enterButton={true}
                />
            </Header>

            {/* Use Antd Row/Col components to display products and cart as columns in the same row*/}
            <Row>
                {/* Display products */}
                <Col
                    xs={{ span: 24 }}
                    md={{ span: loggedIn && products.length ? 18 : 24 }}
                >
                    <div className="search-container ">
                        {/* Display each product item wrapped in a Col component */}
                        <Row>
                            {products.length !== 0 ? (
                                filteredProducts.map((product) => getProductElement(product))
                            ) : loading ? (
                                <div className="loading-text">Loading products...</div>
                            ) : (
                                <div className="loading-text">No products to list</div>
                            )}
                        </Row>
                    </div>
                </Col>

                {/* Display cart */}
                {loggedIn && products.length && (
                    <Col xs={{ span: 24 }} md={{ span: 6 }} className="search-cart">
                        <div>
                            <Cart
                                ref={cartRef}
                                products={products}
                                history={history}
                                token={localStorage.getItem("token")}
                            />
                        </div>
                    </Col>
                )}
            </Row>

            {/* Display the footer */}
            <Footer />
        </>
    );
};

export default Search;

Then the product page is there which is to render product cards with details and a button to add to cart.

/* eslint-disable react/prop-types */
import { PlusCircleOutlined } from "@ant-design/icons";
import { Button, Card, Rate } from "antd";
import "./Product.css";
import { useDispatch, useSelector } from "react-redux";
// import { addToCart } from "../../redux/actions";

/**
 * @typedef {Object} Product
 * @property {string} name - The name or title of the product
 * @property {string} category - The category that the product belongs to
 * @property {number} cost - The price to buy the product
 * @property {number} rating - The aggregate rating of the product (integer out of five)
 * @property {string} image - Contains URL for the product image
 * @property {string} _id - Unique ID for the product
 */

/**
 * The goal is to display an individual product as a card displaying relevant product properties
 * Product image and product title are primary information
 * Secondary information to be displayed includes cost, rating and category
 * We also need a button to add the product to cart from the product listing
 *
 * @param {Product} props.product
 *    The product object to be displayed
 * @param {function} props.addToCart
 *    Function to call when user clicks on a Product card's 'Add to cart' button
 * @returns {JSX}
 *    HTML and JSX to be rendered
 */
export default function Product({ product, addToCart }) {
    // const dispatch = useDispatch();
    // const cart = useSelector((state) => state.cart);

    // const handleAddToCart = (product) => {
    //     dispatch(addToCart(product))
    // }
    return (
        // Use Antd Card component to create a card-like view for individual products
        <Card className="product" hoverable>
            {/* Display product image */}
            <img className="product-image" alt="product" src={product.image} />

            {/* Display product information */}
            <div className="product-info">
                {/* Display product name and category */}
                <div className="product-info-text">
                    <div className="product-title">{product.name}</div>
                    <div className="product-category">{`Category: ${product.category}`}</div>
                </div>

                {/* Display utility elements */}
                <div className="product-info-utility">
                    {/* Display product cost */}
                    <div className="product-cost">{`₹${product.cost}`}</div>

                    {/* Display star rating for the product on a scale of 5 */}
                    <div>
                        <Rate
                            className="product-rating"
                            disabled={true}
                            defaultValue={product.rating}
                        />
                    </div>

                    {/* Display the "Add to Cart" button */}
                    <Button
                        shape="round"
                        type="primary"
                        icon={<PlusCircleOutlined />}
                        onClick={() => addToCart(product)}
                    >
                        Add to Cart
                    </Button>
                </div>
            </div>
        </Card>
    );
}

Then there is cart component which holds functions to manipulate cart like and to interact with backend like postToCart, incrementQty, decrementQty, deleteItem, etc.

/* eslint-disable react/prop-types */
import { ShoppingCartOutlined } from "@ant-design/icons";
import { Button, Card, message, Spin, InputNumber } from "antd";
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { config } from "../../App";
import "./Cart.css";
// import { addToCart, removeFromCart, incrementQuantity, decrementQuantity } from "../../redux/actions";
import { useSelector, useDispatch } from "react-redux";

/**
 * @typedef {Object} Product
 * @property {string} name - The name or title of the product
 * @property {string} category - The category that the product belongs to
 * @property {number} cost - The price to buy the product
 * @property {number} rating - The aggregate rating of the product (integer out of five)
 * @property {string} image - Contains URL for the product image
 * @property {string} _id - Unique ID for the product
 */

/**
 * @typedef {Object} CartItem
 * @property {string} productId - Unique ID for the product
 * @property {number} qty - Quantity of the product in cart
 * @property {Product} product - Corresponding product object for that cart item
 */

const Cart = React.forwardRef(({ products, token, checkout }, ref) => {
    const navigate = useNavigate();
    const [items, setItems] = useState([]);
    const [loading, setLoading] = useState(false);

    // const dispatch = useDispatch();
    // const cart = useSelector((state) => state.cart);

    // const handleAddToCart = (product) => {
    //     dispatch(addToCart(product));
    // }

    // const handleRemoveFromCart = (productId) => {
    //     dispatch(removeFromCart(productId));
    // };

    // const handleIncrementQuantity = (productId) => {
    //     dispatch(incrementQuantity(productId));
    // };

    // const handleDecrementQuantity = (productId) => {
    //     dispatch(decrementQuantity(productId));
    // };

    /**
 * Check the response of the API call to be valid and handle any failures along the way
 *
 * @param {boolean} errored
 *    Represents whether an error occurred in the process of making the API call itself
 * @param {{ productId: string, qty: number }|{ success: boolean, message?: string }} response
 *    The response JSON object which may contain further success or error messages
 * @returns {boolean}
 *    Whether validation has passed or not
 *
 * If the API call itself encounters an error, errored flag will be true.
 * If the backend returns an error, then success field will be false and message field will have a string with error details to be displayed.
 * When there is an error in the API call itself, display a generic error message and return false.
 * When there is an error returned by backend, display the given message field and return false.
 * When there is no error and API call is successful, return true.
 */

    const validateResponse = (errored, response) => {
        if (errored) {
            message.error(
                "Could not update cart."
            );
            return false;
        } else if (response.message) {
            message.error(response.message);
            return false;
        }

        return true;
    };

    /**
 * Perform the API call to fetch the user's cart and return the response
 *
 * @returns {{ productId: string, qty: number }|{ success: boolean, message?: string }}
 *    The response JSON object
 *
 * -    Set the loading state variable to true
 * -    Perform the API call via a fetch call: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
 * -    The call must be made asynchronously using Promises or async/await
 * -    The call must be authenticated with an authorization header containing Oauth token
 * -    The call must handle any errors thrown from the fetch call
 * -    Parse the result as JSON
 * -    Set the loading state variable to false once the call has completed
 * -    Call the validateResponse(errored, response) function defined previously
 * -    If response passes validation, return the response object
 *
 *
 */

    const getCart = async () => {
        let response = {};
        let errored = false;

        setLoading(true);

        try {
            response = await (
                await fetch(`${config.endpoint}/cart`, {
                    method: "GET",
                    headers: {
                        Authorization: `Bearer ${token}`,
                    },
                })
            ).json();
        } catch (e) {
            errored = true;
        }

        setLoading(false);

        if (validateResponse(errored, response)) {
            return response;
        }
    };

    /**
 * Perform the API call to add or update items in the user's cart
 *
 * @param {string} productId
 *    ID of the product that is to be added or updated in cart
 * @param {number} qty
 *    How many of the product should be in the cart
 * @param {boolean} fromAddToCartButton
 *    If this function was triggered from the product card's "Add to Cart" button
 *
 * -    If the user is trying to add from the product card and the product already exists in cart, show an error message
 * -    Set the loading state variable to true
 * -    Perform the API call via a fetch call: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
 * -    The call must be made asynchronously using Promises or async/await
 * -    The call must be authenticated with an authorization header containing Oauth token
 * -    The call must handle any errors thrown from the fetch call
 * -    Parse the result as JSON
 * -    Set the loading state variable to false once the call has completed
 * -    Call the validateResponse(errored, response) function defined previously
 * -    If response passes validation, refresh the cart by calling refreshCart()
 */

    const postToCart = async (productId, qty) => {
        console.log('postToCart call hua bhai!')
        let response = {};
        let errored = false;
        let statusCode;

        setLoading(true);

        try {
            response = await (
                await fetch(`${config.endpoint}/cart`, {
                    method: "POST",
                    headers: {
                        Authorization: `Bearer ${token}`,
                        "Content-Type": "application/json",
                    },
                    body: JSON.stringify({
                        productId: productId,
                        quantity: qty,
                    }),
                })
            ).json();
        } catch (e) {
            errored = true;
        }

        setLoading(false);

        if (validateResponse(errored, response, statusCode)) {
            await refreshCart();
        }
    };

    const putToCart = async (productId, qty) => {
        let response = {};
        let errored = false;
        let statusCode;

        setLoading(true);

        try {
            let response_object = await fetch(`${config.endpoint}/cart`, {
                method: "PUT",
                headers: {
                    Authorization: `Bearer ${token}`,
                    "Content-Type": "application/json",
                },
                body: JSON.stringify({
                    productId: productId,
                    quantity: qty,
                }),
            });

            statusCode = response_object.status;
            if (statusCode !== 204) {
                response = await response_object.json();
            }
        } catch (e) {
            errored = true;
        }

        setLoading(false);

        if (
            statusCode === "204" ||
            validateResponse(errored, response, statusCode)
        ) {
            await refreshCart();
        }
    };

    /**
     * Function to get/refresh list of items in cart from backend and update state variable
     * -    Call the previously defined getCart() function asynchronously and capture the returned value in a variable
     * -    If the returned value exists,
     *      -   Update items state variable with the response (optionally add the corresponding product object of that item as a sub-field)
     
     * -    If the cart is being displayed from the checkout page, or the cart is empty,
     *      -   Display an error message
     *      -   Redirect the user to the products listing page
     
     */

    const refreshCart = async () => {
        const cart = await getCart();
        if (cart && cart.cartItems) {
            setItems(
                cart.cartItems.map((item) => ({
                    ...item,
                    product: products.find((product) => product._id === item.product._id),
                }))
            );
        }
    };

    const calculateTotal = () => {
        return items.length
            ? items.reduce(
                (total, item) => total + item.product.cost * item.quantity,
                0
            )
            : 0;
    };

    const getQuantityElement = (item) => {
        return checkout ? (
            <>
                <div className="cart-item-qty-fixed"></div>
                <div className="cart-item-qty-fixed">Qty: {item.quantity}</div>
            </>
        ) : (
            <InputNumber
                min={0}
                max={10}
                value={item.quantity}
                onChange={(value) => {
                    putToCart(item.product._id, value);
                }}
            />
        );
    };

    useEffect(() => {
        refreshCart();
    }, []);
 

    return (
        <div className={["cart", checkout ? "checkout" : ""].join(" ")}>
            {/* Display cart items or a text banner if cart is empty */}
            {items.length ? (
                <>
                    {/* Display a card view for each product in the cart */}
                    {items.map((item) => (
                        <Card className="cart-item" key={item.productId}>
                            {/* Display product image */}
                            <img
                                className="cart-item-image"
                                alt={item.product.name}
                                src={item.product.image}
                            />
                            {/* Display product details*/}
                            <div className="cart-parent">
                                {/* Display product name, category and total cost */}
                                <div className="cart-item-info">
                                    <div>
                                        <div className="cart-item-name">{item.product.name}</div>
                                        <div className="cart-item-category">
                                            {item.product.category}
                                        </div>
                                    </div>
                                    {/* Display field to update quantity or a static quantity text */}
                                    <div className="cart-item-cost">
                                        ₹{item.product.cost * item.quantity}
                                    </div>
                                </div>
                                <div className="cart-item-qty">{getQuantityElement(item)}</div>
                            </div>
                        </Card>
                    ))}
                    {/* Display cart summary */}
                    <div className="total">
                        <h2>Total</h2>
                        {/* Display net quantity of items in the cart */}
                        <div className="total-item">
                            <div>Products</div>
                            <div>
                                {items.reduce(function (sum, item) {
                                    return sum + item.quantity;
                                }, 0)}
                            </div>
                        </div>
                        {/* Display the total cost of items in the cart */}
                        <div className="total-item">
                            <div>Sub Total</div>
                            <div>₹{calculateTotal()}</div>
                        </div>
                        {/* Display shipping cost */}
                        <div className="total-item">
                            <div>Shipping</div>
                            <div>N/A</div>
                        </div>
                        <hr></hr>
                        {/* Display the sum user has to pay while checking out */}
                        <div className="total-item">
                            <div>Total</div>
                            <div>₹{calculateTotal()}</div>
                        </div>
                    </div>
                </>
            ) : (
                <div className="loading-text">
                    Add an item to cart and it will show up here
                    <br />
                    <br />
                </div>
            )}
            {/* Display a "Checkout" button */}
            {!checkout && (
                <Button
                    className="ant-btn-warning"
                    type="primary"
                    icon={<ShoppingCartOutlined />}
                    onClick={() => {
                        if (items.length) {
                            navigate("/checkout");
                        } else {
                            message.error("You must add items to cart first");
                        }
                    }}
                >
                    <strong> Checkout</strong>
                </Button>
            )}
            {/* Display a loading icon if the "loading" state variable is true */}
            {loading && (
                <div className="loading-overlay">
                    <Spin size="large" />
                </div>
            )}
        </div>
    );
});

Cart.displayName = 'Cart';
export default Cart;
1

There are 1 answers

0
Drew Reese On

The Cart component doesn't do anything with the ref that it forwards. That said, you could make it so Cart exposes out the postToCart handler so the parent component can call it... but this is a bit of a React anti-pattern... React components don't reach into and call internal functions of other React components. It would be better to rethink your structure and pass postToCart down as a prop or reorganize the code so the parent component calls a postToCart with the correct data.

It appears you should just move this postToCart function to the Search component where it can call it in scope.

const Search = () => {
  const navigate = useNavigate();

  const [loading, setLoading] = useState(false);
  const [loggedIn, setLoggedIn] = useState(false);
  const [filteredProducts, setFilteredProducts] = useState([]);
  const [products, setProducts] = useState([]);
  const [debounceTimeout, setDebounceTimeout] = useState(0);

  ...

  const postToCart = async (productId, qty) => {
    ...
  };

  ...

  const getProductElement = (product) => {
    return (
      <Col xs={24} sm={12} xl={6} key={product._id}>
        <Product
          product={product}
          addToCart={() => {
            if (loggedIn) {
              postToCart(product._id, 1, true);
            } else {
              navigate("/login");
            }
          }}
        />
      </Col>
    );
  };

  ...
};