有什么是useReducer可以做而useState不能做的?

问题描述 投票:0回答:1

我在 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;
reactjs typescript firebase react-hooks react-state-management
1个回答
0
投票

区别似乎在于,基于

useState
useProductDetails
setProductDetails
上的循环中调用 productDocumentsSnapshot.docs
inside
,而基于
useReduce
的钩子则通过
dispatch
after
调用 productData循环。

这会导致问题,因为每次都会使用

same
setProductDetails 来调用您
productsData
。 React 会假设状态实际上没有改变,并且不会重新渲染你的组件。它不会查看数组内部来检查是否包含与以前不同的元素。要解决这个问题,

  • 在每次迭代期间创建一个新数组,而不是
    push
    创建同一个数组,例如
    productsData = [...productsData, { id: productId, data: productDetails }];
  • 或者在循环完成后仅更改状态一次
© www.soinside.com 2019 - 2024. All rights reserved.