我在 Firestore 中存储了一组产品详细信息。我创建了一个自定义挂钩来获取产品详细信息。产品类别应显示在匹配的 url 参数中。当我添加更多要在其他网址中显示的产品类别时,产品卡不会在安装时呈现。当我尝试编辑产品卡组件时,产品卡会在几秒钟内呈现并消失。数据已正确获取并正确渲染,但渲染的产品卡在几秒钟内消失。我无法弄清楚问题是什么,因为控制台中没有错误。会不会跟状态管理有关?
编辑:我尝试在自定义挂钩中使用 useReducer 而不是 useState ,现在一切正常。我的问题是是什么让 useReducer 工作而不是 useState 因为这两个钩子做同样的事情。是因为产品详情的数据结构复杂吗?
//The custom hook for fetching product details using useState
import { db } from "../../firebaseConfig";
import { collection, getDocs } from "firebase/firestore";
import { useState, useEffect } from "react";
interface ProductDetails {
name: string;
imageURLs: string[];
rating: number;
reviewsCount: number;
monthlySalesCount: number;
isBestseller: boolean;
listPrice: number;
discount: number;
shippingPrice: number;
Color: string[];
productDescription: string[];
dateFirstAvailable: Date;
}
interface Data {
id: string;
data: ProductDetails[];
}
const useProductDetails = () => {
const [productDetails, setProductDetails] = useState<Data[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchProductDetails = async () => {
try {
// Fetch all categories from the products collection.
const productCategories = collection(db, "products");
const productDocumentsSnapshot = await getDocs(productCategories);
// An array to store product details
const productsData: Data[] = [];
// Iterate over each document in the products collection.
for (const productDoc of productDocumentsSnapshot.docs) {
const productId = productDoc.id;
// For each product category, fetch all documents from the productslist subcollection.
const productListCollection = collection(
db,
`products/${productId}/productslist`
);
const productsListSnapshot = await getDocs(productListCollection);
// Map over the documents in productslist to extract ProductDetails data.
const productDetails: ProductDetails[] =
productsListSnapshot.docs.map(
(detailDoc) => detailDoc.data() as ProductDetails
);
// Push an object with id and data to productsData.
productsData.push({
id: productId,
data: productDetails,
});
// Update the productDetails state with the fetched data.
setProductDetails(productsData);
console.log("Updated products state:", productsData);
}
} catch (error) {
console.error("Error fetching products:", error);
} finally {
setLoading(false);
}
};
fetchProductDetails();
}, []);
return { loading, productDetails };
};
export default useProductDetails;
//The custom hook for fetching product details using useReducer & this worked!
import { useReducer, useEffect } from "react";
import { db } from "../../firebaseConfig";
import { collection, getDocs } from "firebase/firestore";
interface ProductDetails {
name: string;
imageURLs: string[];
rating: number;
reviewsCount: number;
monthlySalesCount: number;
isBestseller: boolean;
listPrice: number;
discount: number;
shippingPrice: number;
Color: string[];
Size?: string;
Style?: string;
Pattern?: string;
Brand: string;
earPlacement?: string;
formFactor?: string;
noiseControl?: string;
impedance?: number;
compatibleDevices?: string;
specialFeature?: string;
productDescription: string[];
dateFirstAvailable: Date;
}
interface Data {
id: string;
data: ProductDetails[];
}
interface State {
productDetails: Data[];
loading: boolean;
error: string | null;
}
type Action =
| { type: "FETCH_PRODUCTS_REQUEST" }
| { type: "FETCH_PRODUCTS_SUCCESS"; payload: Data[] }
| { type: "FETCH_PRODUCTS_FAILURE"; payload: string };
// Define initial state
const initialState: State = {
productDetails: [],
loading: true,
error: null,
};
// Define reducer function
function productDetailsReducer(state: State, action: Action): State {
switch (action.type) {
case "FETCH_PRODUCTS_REQUEST":
return {
...state,
loading: true,
error: null,
};
case "FETCH_PRODUCTS_SUCCESS":
return {
...state,
loading: false,
productDetails: action.payload,
};
case "FETCH_PRODUCTS_FAILURE":
return {
...state,
loading: false,
error: action.payload,
};
default:
return state;
}
}
// Fetch products function
const fetchProductDetails = async (dispatch: React.Dispatch<Action>) => {
dispatch({ type: "FETCH_PRODUCTS_REQUEST" });
try {
const productCategories = collection(db, "products");
const productDocumentsSnapshot = await getDocs(productCategories);
const productsData: Data[] = [];
for (const productDoc of productDocumentsSnapshot.docs) {
const productId = productDoc.id;
const productListCollection = collection(
db,
`products/${productId}/productslist`
);
const productsListSnapshot = await getDocs(productListCollection);
const productDetails = productsListSnapshot.docs.map(
(detailDoc) => detailDoc.data() as ProductDetails
);
productsData.push({
id: productId,
data: productDetails,
});
}
dispatch({ type: "FETCH_PRODUCTS_SUCCESS", payload: productsData });
} catch (error: any) {
dispatch({ type: "FETCH_PRODUCTS_FAILURE", payload: error.message });
}
};
// Custom hook
const useProductDetails = () => {
const [state, dispatch] = useReducer(productDetailsReducer, initialState);
useEffect(() => {
fetchProductDetails(dispatch);
}, []);
return {
loading: state.loading,
productDetails: state.productDetails,
error: state.error,
};
};
export default useProductDetails;
//The code for rendering the product card
import StarRating from "../sidebar/StarRating";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import useProductDetails from "../../useProductDetails";
import RenderColourOptions from "../../RenderColourOptions";
import { Link } from "react-router-dom";
import useFetchCountry from "../../../useFetchCountry";
interface ProductListProps {
id: string | undefined;
}
function ProductCard(props: ProductListProps) {
const { loading, productDetails } = useProductDetails();
const { country } = useFetchCountry();
if (loading) {
return <p>Loading...</p>;
}
const calculateDiscountedPrice = (
listPrice: number,
discount: number
): string => {
const discountedPrice = (listPrice - listPrice * (discount / 100)).toFixed(
2
);
return discountedPrice;
};
const renderFormattedPrice = (price: string | number) => {
const priceString = typeof price === "number" ? price.toString() : price;
const [wholeNumber, fractional] = priceString.split(".");
return (
<>
<span className="text-clamp16 ">{wholeNumber}</span>
{fractional === "00" ? null : (
<sup>
<span className="text-clamp6 align-super">{fractional}</span>
</sup>
)}
</>
);
};
const formatSalesCount = (num: number) => {
if (num >= 1000) {
const formattedSalesCount = new Intl.NumberFormat("en-US", {
notation: "compact",
}).format(num);
return `${formattedSalesCount}+`;
} else return num;
};
const calculateDeliveryDate = () => {
const currentDate = new Date();
currentDate.setDate(currentDate.getDate() + 20);
return currentDate.toLocaleString("en-US", {
weekday: "short",
month: "short",
day: "numeric",
});
};
return (
<>
{productDetails.map((product) => {
if (product.id === props.id) {
return product.data.map((details) => (
<div
key={product.id}
className="mr-[1%] mb-[1%] flex border-[1px] border-gray-100 rounded-[6px]"
>
<div className="bg-gray-100 w-[25%] rounded-l-[4px]">
<img
src={details.imageURLs[0]}
alt={details.name}
className="mix-blend-multiply py-[15%] px-[5%]"
key={details.name}
/>
</div>
<div className="bg-white w-[75%] rounded-r-[4px] pl-[1.5%]">
<Link to="">
<h1 className="text-clamp15 my-[0.75%] line-clamp-2">
{details.name}
</h1>
</Link>
<div className="flex items-center -mt-[0.75%]">
<StarRating
rating={details.rating}
fontSize="clamp(0.5625rem, 0.2984rem + 1.1268vi, 1.3125rem)"
/>
<KeyboardArrowDownIcon
style={{
fontSize:
"clamp(0.375rem, 0.1109rem + 1.1268vi, 1.125rem)",
}}
className="-ml-[1.75%] text-gray-400"
/>
<p className="text-clamp11 text-cyan-800 font-sans">
{details.reviewsCount.toLocaleString()}
</p>
</div>
<p className="text-clamp13 text-gray-700 mb-[1%]">{`${formatSalesCount(
details.monthlySalesCount
)} bought in past month`}</p>
<div className="flex items-baseline">
<p>
{details.discount ? (
<>
<sup>
<span className="text-clamp10 align-super">$</span>
</sup>
{renderFormattedPrice(
calculateDiscountedPrice(
details.listPrice,
details.discount
)
)}
</>
) : (
<>
<sup>
<span className="text-clamp10 align-super">$</span>
</sup>
{renderFormattedPrice(details.listPrice)}
</>
)}
</p>
{details.discount ? (
<p className=" text-clamp13 text-gray-700 ml-[1%]">
List:
<span className="line-through ml-[5%]">
${details.listPrice}
</span>
</p>
) : null}
</div>
<p className="mt-[1%] text-clamp13">
Delivery{" "}
<span className="font-bold tracking-wide">
{calculateDeliveryDate()}
</span>
</p>
<p className="text-clamp1 mt-[0.5%]">Ships to {country}</p>
<button className="bg-yellow-400 px-[1.75%] py-[0.5%] mt-[1%] rounded-[25px] text-clamp10">
Add to cart
</button>
<RenderColourOptions colors={details.Color} />
</div>
</div>
));
}
return null;
})}
</>
);
}
export default ProductCard;
区别似乎在于,基于
useState
的 useProductDetails
在 setProductDetails
上的循环中调用 productDocumentsSnapshot.docs
inside,而基于
useReduce
的钩子则通过 dispatch
after调用
productData
循环。
这会导致问题,因为每次都会使用
same
setProductDetails
来调用您 productsData
。 React 会假设状态实际上没有改变,并且不会重新渲染你的组件。它不会查看数组内部来检查是否包含与以前不同的元素。要解决这个问题,
push
创建同一个数组,例如productsData = [...productsData, { id: productId, data: productDetails }];