javascript reactjs项目中父组件和子组件中ref和useRef的用法,它只给出默认值

epfja78i  于 5个月前  发布在  Java
关注(0)|答案(1)|浏览(69)

我的项目是关于显示产品和购物车,然后结帐。我附上下面的代码片段的三个组成部分是:搜索(主页显示产品),产品组件和购物车。
现在产品渲染正确,但当我点击产品的添加到购物车按钮时,它没有渲染任何东西,cartRef.current正在记录:

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

字符串
有没有人可以帮助我理解我错在哪里?在我看来,我认为这与在Cart组件中使用ref有关。
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;


然后是产品页面,这是呈现产品卡的细节和一个按钮添加到购物车。

/* 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>
    );
}


然后是cart组件,它包含操作cart的函数,并与后端交互,如postToCart,incrementQty,decrementQty,deleteItem等。

/* 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;

ycggw6v2

ycggw6v21#

Cart组件不对它转发的引用做任何事情。也就是说,你可以让Cart暴露出postToCart处理程序,这样父组件就可以调用它. pattern. React组件不会触及和调用其他React组件的内部函数。最好重新考虑您的结构并传递postToCart作为一个 prop ,或者重新组织代码,这样父组件就可以用正确的数据调用postToCart
看起来你应该把这个postToCart函数移到Search组件中,在那里它可以在作用域中调用它。

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>
    );
  };

  ...
};

字符串

相关问题