React组件偶发无限重渲染循环问题排查求助
问题描述
使用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

