You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Query结合WooCommerce API加载商品重复问题排查求助

问题排查与修复:React Query无限加载商品重复渲染

问题背景

WordPress站点包含10000+商品,使用TypeScript和React Query实现点击加载更多的无限滚动功能,点击加载按钮时出现现有商品重复渲染的问题。

问题根源分析

  1. 分页参数类型不匹配:getNextPageParam返回完整URL字符串,但fetchProducts期望接收数字类型页码,导致每次加载下一页时实际请求第一页数据,重复追加相同商品。
  2. QueryKey不稳定:使用[options]作为queryKey,而options是router.query展开的对象,每次组件渲染时对象引用变化,触发React Query重新请求数据,重复加载已有商品。
  3. 缺少商品去重逻辑:未处理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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 07:20:56