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

React组件偶发无限重渲染循环问题排查求助

React Query + TanStack Table 组件无限重渲染排查与解决

问题描述

使用React Query和TanStack Table的React组件偶尔陷入无限重渲染循环,问题间歇性出现,难以定位根源。

相关代码

主组件代码

const { companyDataAll, companyDataAllLoading, companyDataAllError } =
  useCompanyDataAll();

const {
  data: productData,
  isLoading: productLoading,
  isError: productError,
} = useQuery({
  queryKey: ['productList', companyId],
  queryFn: () => productList({ company_id: companyId as number }),
  enabled: !!companyId,
});

const { mutate: deleteMutate } = useMutation({
  mutationFn: productDelete,
  onSuccess: () => {
    toast.success('Ürün başarıyla silindi!');
    queryClient.invalidateQueries({ queryKey: ['productList', companyId] });
  },
  onError: () => {
    toast.error('Ürün silinirken hata oluştu.');
  },
});

const onDelete = useCallback(
  (productId: number) => {
    deleteMutate(productId);
  },
  [deleteMutate]
);

const columns = ProductColumns({
  companyData: companyDataAll || [],
  setSelectedProduct,
  setIsUpdateOpen,
  onDelete,
});

const table = useReactTable({
  data: productData?.results || [],
  columns,
  getCoreRowModel: getCoreRowModel(),
  getFilteredRowModel: getFilteredRowModel(),
  getSortedRowModel: getSortedRowModel(),
  state: {
    globalFilter,
  },
  onGlobalFilterChange: setGlobalFilter,
});

ProductColumns.tsx

interface ProductColumnsProps {
  companyData: CompanyType[];
  setSelectedProduct: React.Dispatch<React.SetStateAction<ProductType | null>>;
  setIsUpdateOpen: React.Dispatch<React.SetStateAction<boolean>>;
  onDelete: (productId: number) => void;
}

const columnHelper = createColumnHelper<ProductType>();

function ProductColumns({
  companyData,
  setSelectedProduct,
  setIsUpdateOpen,
  onDelete,
}: ProductColumnsProps) {
  return useMemo(
    () => [
      columnHelper.accessor((row) => row.company, {
        id: 'company',
        header: 'Firma',
        cell: (info) => {
          const firmId = info.getValue();
          const company = companyData.find((c) => c.id === firmId);
          return (
            <span className='font-semibold text-xs'>
              {company ? company.title : 'Bilinmiyor'}
            </span>
          );
        },
      }),
      columnHelper.accessor((row) => row.product_type, {
        id: 'product_type',
        header: 'Ürün Tipi',
        cell: (info) => {
          const engVal = info.getValue();
          const matchedType = PRODUCT_TYPES.find((pt) => pt.value === engVal);
          const turkishLabel = matchedType ? matchedType.label : engVal;
          return <span className='font-semibold text-xs'>{turkishLabel}</span>;
        },
      }),
      columnHelper.accessor((row) => row.name, {
        id: 'name',
        header: 'Ürün Adı',
        cell: (info) => (
          <span className='font-semibold text-xs'>{info.getValue()}</span>
        ),
      }),
      columnHelper.accessor((row) => row.unit_of_measure, {
        id: 'unit_of_measure',
        header: 'Ölçü Birimi',
        cell: (info) => (
          <span className='font-semibold text-xs'>{info.getValue()}</span>
        ),
      }),
      columnHelper.display({
        id: 'update',
        header: '',
        cell: ({ row }) => {
          return (
            <Button
              variant='default'
              size='sm'
              onClick={() => {
                const product = row.original;
                setSelectedProduct(product);
                setIsUpdateOpen(true);
              }}
            >
              Güncelle
            </Button>
          );
        },
        meta: {
          className: 'w-[120px]',
        },
      }),
      columnHelper.display({
        id: 'delete',
        header: '',
        cell: ({ row }: { row: Row<ProductType> }) => (
          <Button
            size='sm'
            variant='destructive'
            onClick={() => {
              const product = row.original;

              if (
                confirm(
                  `${product.name} isimli ürünü silmek istediğinize emin misiniz?`
                )
              ) {
                onDelete(product.id);
              }
            }}
          >
            Sil
          </Button>
        ),
        meta: {
          className: 'w-[120px]',
        },
      }),
    ],
    [companyData, setSelectedProduct, setIsUpdateOpen, onDelete]
  );
}

export default ProductColumns;

useCompanyDataAll.ts

import { companyListAll } from '@/lib/redux/features/company/companyApi';
import { useQuery } from '@tanstack/react-query';

export const useCompanyDataAll = () => {
  const {
    data: companyDataAll,
    isLoading: companyDataAllLoading,
    isError: companyDataAllError,
  } = useQuery({
    queryKey: ['companyListAll'],
    queryFn: companyListAll,
  });

  console.log('companyDataAll hook', companyDataAll);

  return {
    companyDataAll,
    companyDataAllLoading,
    companyDataAllError,
  };
};

排查与解决步骤

1. 修复companyDataAll的空数组引用问题

主组件中companyDataAll || []会在companyDataAll为undefined时,每次渲染创建新的空数组,导致ProductColumns的useMemo依赖companyData频繁变化,进而重新生成columns,触发useReactTable重新计算,引发重渲染循环。

解决方法:在useCompanyDataAll中直接给data设置默认空数组,避免每次渲染创建新数组:

export const useCompanyDataAll = () => {
  const {
    data: companyDataAll = [], // 添加默认空数组
    isLoading: companyDataAllLoading,
    isError: companyDataAllError,
  } = useQuery({
    queryKey: ['companyListAll'],
    queryFn: companyListAll,
  });

  console.log('companyDataAll hook', companyDataAll);

  return {
    companyDataAll,
    companyDataAllLoading,
    companyDataAllError,
  };
};

之后主组件中直接传递companyDataAll即可:

const columns = ProductColumns({
  companyData: companyDataAll,
  setSelectedProduct,
  setIsUpdateOpen,
  onDelete,
});

2. 确保productDelete函数的稳定性

useMutation的mutationFn如果每次渲染都重新创建,会导致deleteMutate变化,进而触发onDelete(依赖deleteMutate)变化,最终导致columns重新生成。

解决方法:如果productDelete是定义在组件内部的函数,用useCallback包裹它,确保引用稳定:

const productDelete = useCallback(async (productId: number) => {
  // 原productDelete逻辑
}, []);

如果是外部导入的函数,本身引用稳定,无需额外处理。

3. 优化useReactTable的状态依赖

useReactTable的state参数中,确保globalFilter是稳定的状态值(比如来自useState),避免每次渲染传递新的对象。当前代码中state: { globalFilter }如果globalFilter是useState的状态,这部分没问题。

4. 检查companyId的稳定性

如果companyId频繁变化,会触发productList查询重复执行,导致productData频繁更新,引发组件重渲染。确保companyId的更新逻辑是合理的,不会出现无意义的循环变化。

5. 移除不必要的日志

useCompanyDataAll中的console.log虽然不会直接导致重渲染,但频繁打印会干扰调试,建议注释或移除。

内容的提问来源于stack exchange,提问作者Berkay Çelikkol

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 20:14:53