У меня есть набор сведений о продукте, хранящийся в Firestore. Я создал специальный крючок для получения сведений о продукте. Категории продуктов должны отображаться в соответствующем параметре URL. Когда я добавляю дополнительные категории продуктов для отображения в других URL-адресах, карточки продуктов не отображаются при монтировании. Когда я пытаюсь отредактировать компонент productcard
, карточка товара отображается и исчезает за считанные секунды. Данные извлекаются правильно и отображаются правильно, но отображаемая карточка продукта исчезает через несколько секунд. Я не могу понять, в чем проблема, поскольку в консоли нет ошибок. Может ли это быть что-то связанное с государственным управлением?
Обновлено: я пробовал использовать useReducer
вместо useState
в пользовательском хуке, и теперь все работает отлично. Мой вопрос в том, что заставляет useReducer
работать, а не useState
, потому что оба этих хука делают одно и то же. Это из-за сложности структуры данных о деталях продукта?
Пользовательский хук для получения сведений о продукте с помощью 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.info("Updated products state:", productsData);
}
} catch (error) {
console.error("Error fetching products:", error);
} finally {
setLoading(false);
}
};
fetchProductDetails();
}, []);
return { loading, productDetails };
};
export default useProductDetails;
Специальный хук для получения сведений о продукте с помощью useReducer, и это сработало!
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;
Код для отрисовки карточки товара
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;
🤔 А знаете ли вы, что...
React поддерживает создание контекста для передачи данных между компонентами.
Разница в том, что ваш useState
на основе useProductDetails
вызывает setProductDetails
внутри цикла над productDocumentsSnapshot.docs
, тогда как крючок на основе useReduce
делает dispatch
с productData
после цикла.
Это вызывает проблему, потому что вам setProductDetails
каждый раз звонят с одним и тем же productsData
. React будет считать, что состояние на самом деле не изменилось, и не будет повторно отображать ваш компонент. Он не будет заглядывать внутрь массива, чтобы проверить, содержит ли он другие элементы, чем раньше. Чтобы исправить это,
push