React Query结合WooCommerce API加载商品重复问题排查求助
问题排查与修复:React Query无限加载商品重复渲染
问题背景
WordPress站点包含10000+商品,使用TypeScript和React Query实现点击加载更多的无限滚动功能,点击加载按钮时出现现有商品重复渲染的问题。
问题根源分析
- 分页参数类型不匹配:
getNextPageParam返回完整URL字符串,但fetchProducts期望接收数字类型页码,导致每次加载下一页时实际请求第一页数据,重复追加相同商品。 - QueryKey不稳定:使用
[options]作为queryKey,而options是router.query展开的对象,每次组件渲染时对象引用变化,触发React Query重新请求数据,重复加载已有商品。 - 缺少商品去重逻辑:未处理WooCommerce API可能返回的重复商品数据(如分页边界异常),导致重复渲染。
代码修复
1. 修正分页逻辑与QueryKey(fetch-get-all-products.tsx)
import { QueryOptionsType, Product } from "@framework/types"; import { useInfiniteQuery, QueryKey, QueryFunctionContext } from "@tanstack/react-query"; import WooCommerceRestApi from "@woocommerce/woocommerce-rest-api"; // 初始化WooCommerce API客户端 const WooCommerce = new WooCommerceRestApi({ url: process.env.NEXT_PUBLIC_WORDPRESS_SITE_URL, consumerKey: process.env.WC_CONSUMER_KEY, consumerSecret: process.env.WC_CONSUMER_SECRET, version: "wc/v3", }); // 定义分页响应类型,替换any类型提升类型安全 type ProductPage = { data: Product[]; paginatorInfo: { nextPage: number | null; totalPages: number; }; }; // 获取商品列表,参数明确为页码和每页数量 const fetchProducts = async ({ pageParam = 1, perPage = 100 }: { pageParam?: number; perPage?: number }) => { try { const { data, headers } = await WooCommerce.get(`products?per_page=${perPage}&page=${pageParam}&status=publish`); // 从响应头提取总页数(WooCommerce API返回x-wp-total-pages) const totalPages = parseInt(headers["x-wp-total-pages"] || "0", 10); const nextPage = pageParam < totalPages ? pageParam + 1 : null; return { data: data, paginatorInfo: { nextPage, totalPages, }, }; } catch (error: any) { // 抛出具体错误信息,便于调试 throw new Error(`获取商品失败: ${error.message || "未知错误"}`); } }; // 自定义商品查询Hook,优化QueryKey稳定性 const useProductsQuery = (options: QueryOptionsType) => { // 提取options中的具体参数作为QueryKey,避免对象引用变化触发重复请求 const queryKeyParts = Object.entries(options) .sort(([a], [b]) => a.localeCompare(b)) .flat(); return useInfiniteQuery<ProductPage, Error>({ queryKey: ["products", ...queryKeyParts], queryFn: async ({ pageParam = 1 }: QueryFunctionContext<QueryKey, unknown>) => { return fetchProducts({ pageParam: pageParam as number }); }, initialPageParam: 1, // 返回数字类型的下一页页码,匹配fetchProducts参数要求 getNextPageParam: ({ paginatorInfo }) => paginatorInfo.nextPage, }); }; export { useProductsQuery, fetchProducts };
2. 添加去重逻辑与优化渲染(Product-Grid.tsx)
import React, { useMemo } from 'react'; import ProductCard from "@components/product/product-card"; import Button from "@components/ui/button"; import type { FC } from "react"; import { useProductsQuery } from "@framework/product/get-all-products"; import { useRouter } from "next/router"; import ProductFeedLoader from "@components/ui/loaders/product-feed-loader"; import { useTranslation } from "next-i18next"; import { Product } from "@framework/types"; interface ProductGridProps { className?: string; } export const ProductGrid: FC<ProductGridProps> = ({ className = "" }) => { const { query } = useRouter(); const { isInitialLoading: isLoading, // 使用isInitialLoading判断初始加载,避免加载下一页时显示loader isFetchingNextPage: loadingMore, fetchNextPage, hasNextPage, data, error, } = useProductsQuery({ ...query }); const { t } = useTranslation("common"); // 使用useMemo缓存过滤和去重后的商品列表,避免重复计算 const filteredProducts = useMemo(() => { if (!data?.pages) return []; // 合并所有页的商品并去重 const allProducts = data.pages.flatMap(page => page.data || []); const uniqueProducts = Array.from(new Map(allProducts.map(product => [product.id, product])).values()); // 价格过滤逻辑 if (!query.price) return uniqueProducts; const [minPriceStr, maxPriceStr] = (query.price as string).split("-"); const minPrice = parseFloat(minPriceStr); const maxPrice = maxPriceStr ? parseFloat(maxPriceStr) : Infinity; return uniqueProducts.filter(product => { const productPrice = parseFloat(product.price); return !isNaN(productPrice) && productPrice >= minPrice && productPrice < maxPrice; }); }, [data, query.price]); if (error) return <p className="text-red-500">{error.message}</p>; return ( <> {filteredProducts.length === 0 && !isLoading && ( <div className="py-4 text-red-500 font-medium text-sm md:text-sm"> {t("no-products-found") || "未找到符合条件的商品"} </div> )} <div className={`grid grid-cols-2 sm:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-x-3 lg:gap-x-5 xl:gap-x-7 gap-y-3 xl:gap-y-5 2xl:gap-y-8 ${className}`} > {isLoading ? ( <ProductFeedLoader uniqueKey="search-product" /> ) : ( filteredProducts.map(product => ( <ProductCard key={`product-${product.id}`} // 简化key命名,保持唯一性 product={product} variant="grid" id={product.id} // 传递真实商品ID,避免硬编码0 /> )) )} </div> <div className="text-center pt-8 xl:pt-14"> {hasNextPage && ( <Button loading={loadingMore} disabled={loadingMore} onClick={() => fetchNextPage()} variant="slim" > {t("button-load-more")} </Button> )} </div> </> ); };
优化建议
变量命名优化
- 将
PaginatedProduct改为ProductPage,更直观表达单页商品数据的含义 fetchProducts的参数limit改为perPage,与WooCommerce API的per_page参数名保持一致,减少歧义- 组件内变量
isLoading替换为isInitialLoading(React Query内置状态),明确区分初始加载和后续加载状态
注释与类型安全优化
- 为关键逻辑添加注释,比如价格过滤规则、分页参数处理逻辑
- 替换
paginatorInfo的any类型为具体类型定义,提升代码可维护性 - 为
fetchProducts的错误处理添加具体错误信息,便于调试
性能优化
- 使用
useMemo缓存过滤后的商品列表,避免每次组件渲染都重新执行合并、去重、过滤操作 - 简化
ProductCard的key为product-${product.id},保持唯一性的同时更简洁
内容的提问来源于stack exchange,提问作者Mohamed amaan
相关产品推荐
相关产品推荐

