我的项目是关于显示产品和购物车,然后结帐。我附上下面的代码片段的三个组成部分是:搜索(主页显示产品),产品组件和购物车。
现在产品渲染正确,但当我点击产品的添加到购物车按钮时,它没有渲染任何东西,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;
型
1条答案
按热度按时间ycggw6v21#
Cart
组件不对它转发的引用做任何事情。也就是说,你可以让Cart
暴露出postToCart
处理程序,这样父组件就可以调用它. pattern. React组件不会触及和调用其他React组件的内部函数。最好重新考虑您的结构并传递postToCart
作为一个 prop ,或者重新组织代码,这样父组件就可以用正确的数据调用postToCart
。看起来你应该把这个
postToCart
函数移到Search
组件中,在那里它可以在作用域中调用它。字符串