Tanstack Table分页/排序变更触发无限循环问题求助
Tanstack Table 分页/排序触发无限循环问题分析与解决
问题描述
基于Tanstack Table构建的可复用Table组件,在业务组件中使用时,更新分页或排序状态会触发无限循环。组件及业务代码如下:
Table组件代码
import { Box, Stack } from '@mui/material'; import type { SortingState, OnChangeFn, PaginationState } from '@tanstack/react-table'; import { useReactTable, getCoreRowModel, flexRender, getSortedRowModel, getPaginationRowModel, } from '@tanstack/react-table'; import Text from '@components/Text'; import SortIconTable from '@components/SortIconTable'; import { useMemo, useState } from 'react'; export type PaginationShape = { cursor?: string | null | undefined; page: number; pageSize: number; }; export type TableProps<TData> = { columns: TData[]; cursor?: string | null | undefined; loading?: boolean; onPagination?: OnChangeFn<PaginationShape>; onSort?: OnChangeFn<SortingState>; pagination?: PaginationState; rows: TData[]; }; export type TablePropsColumns<TData> = TableProps<TData>['columns']; const pageSizes = [10, 20, 50, 100, 500]; export const Table = ({ rows, columns, onSort, onPagination, cursor }: TableProps<any>) => { const [sorting, setSorting] = useState<SortingState>([]); const [pagination, setPagination] = useState<PaginationState>({ pageIndex: 0, pageSize: 10 }); const memoizedRows = useMemo(() => rows, [rows]); const memoizedColumns = useMemo(() => columns, [columns]); const table = useReactTable({ autoResetPageIndex: true, columnResizeMode: 'onChange', columns: memoizedColumns, data: memoizedRows, debugColumns: true, debugHeaders: true, debugTable: true, enableColumnFilters: true, enableColumnResizing: true, enableMultiSort: false, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), manualPagination: true, manualSorting: true, onPaginationChange: async updater => { if (typeof updater !== 'function') return; const newPageInfo = await updater(table.getState().pagination); setPagination(() => newPageInfo); const formatPaginationData = { cursor, page: newPageInfo?.pageIndex, pageSize: newPageInfo?.pageSize, }; onPagination?.(formatPaginationData); }, onSortingChange: data => { setSorting(data); onSort?.(data); }, sortDescFirst: false, state: { pagination, sorting, }, }); return ( <div className="p-2"> <Box sx={{ position: 'relative' }}> <table style={{ maxWidth: '100%' }}> <thead> {table.getHeaderGroups().map(headerGroup => ( <tr key={headerGroup.id}> {headerGroup.headers.map(header => { const canResize = header.column.getCanResize(); const canSort = header.column.getCanSort(); return ( <th colSpan={header.colSpan} key={header.id} style={{ width: header.getSize() }}> <Stack direction={'row'}> {flexRender(header.column.columnDef.header, header.getContext())} {canSort && ( <SortIconTable onSort={() => { table.setSorting([ { desc: !header.column.getIsSorted(), id: header.column.id, }, ]); header.column.getToggleSortingHandler(); }} sortDirection={header.column.getNextSortingOrder()} /> )} {canResize && ( <Box className={`resizer ${header.column.getIsResizing() && 'isResizing'}`} onDoubleClick={() => header.column.resetSize()} onMouseDown={header.getResizeHandler()} sx={{ '&.isResizing': { background: 'blue', cursor: 'col-resize', opacity: 1, }, '&:hover': { cursor: 'col-resize', }, border: '1px solid gray', ml: 'auto', }} /> )} </Stack> </th> ); })} </tr> ))} </thead> <tbody> {table.getRowModel().rows.map(row => ( <tr key={row.id}> {row.getVisibleCells().map(cell => ( <td key={cell.id} style={{ width: cell.column.getSize() }}> <Text>{flexRender(cell.column.columnDef.cell, cell.getContext())}</Text> </td> ))} </tr> ))} </tbody> <div> <select onChange={e => { table.setPagination({ pageIndex: 0, pageSize: Number(e.target.value), }); }} value={table.getState().pagination.pageSize} > {pageSizes.map(pageSize => ( <option key={pageSize} value={pageSize}> {pageSize} </option> ))} </select> </div> </table> </Box> </div> ); };
业务组件使用代码
const [sorting, setSorting] = useState<SortingState>([]); const [pagination, setPagination] = useState<PaginationShape>({ page: 0, pageSize: 10 }); const params = { order_by: buildSortByString(sorting), page_size: pagination.pageSize, }; console.log({ params }); const { data } = useQuery<EventsLogResponse>({ queryFn: () => getEventsLog({ params }), queryKey: ['getEventsLog', { params }], }); <CustomTable columns={customColumns} cursor={data?.pagination_information.next_cursor} onPagination={setPagination} onSort={setSorting} rows={data?.events ?? []} />
根源排查
- 双状态冲突:Table组件内部维护了
sorting和pagination状态,同时外部业务组件也维护了一份相同逻辑的状态。当内部状态更新时触发外部回调更新业务组件状态,业务组件状态变化又会重新渲染Table组件,导致循环更新。 - 不稳定的queryKey:业务组件中
params是每次渲染都新建的对象,导致useQuery的queryKey每次都变化,重复触发数据请求。请求完成后更新rows,触发Table组件重新渲染,进而再次触发状态更新。 - 重复的排序触发逻辑:Table组件中排序按钮同时调用了
table.setSorting和header.column.getToggleSortingHandler(),导致两次触发排序状态更新,加剧循环。 - 不稳定的columns引用:如果业务组件中的
customColumns是每次渲染都新建的数组,即使使用了useMemo缓存,也会因为引用变化导致Table组件重新计算,触发不必要的更新。
解决建议
1. 统一状态管理,改为受控组件
去掉Table组件内部的sorting和pagination状态,完全由外部业务组件传入并控制,确保状态单一来源:
// 修改TableProps,新增sorting和pagination必填项 export type TableProps<TData> = { columns: TData[]; cursor?: string | null | undefined; loading?: boolean; onPagination?: OnChangeFn<PaginationShape>; onSort?: OnChangeFn<SortingState>; pagination: PaginationState; rows: TData[]; sorting: SortingState; }; // 修改Table组件实现 export const Table = ({ rows, columns, onSort, onPagination, cursor, sorting, pagination }: TableProps<any>) => { const memoizedRows = useMemo(() => rows, [rows]); const memoizedColumns = useMemo(() => columns, [columns]); const table = useReactTable({ autoResetPageIndex: true, columnResizeMode: 'onChange', columns: memoizedColumns, data: memoizedRows, debugColumns: true, debugHeaders: true, debugTable: true, enableColumnFilters: true, enableColumnResizing: true, enableMultiSort: false, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), manualPagination: true, manualSorting: true, onPaginationChange: async updater => { if (typeof updater !== 'function') return; // 使用外部传入的pagination作为初始值 const newPageInfo = await updater(pagination); const formatPaginationData = { cursor, page: newPageInfo?.pageIndex, pageSize: newPageInfo?.pageSize, }; onPagination?.(formatPaginationData); }, onSortingChange: data => { // 直接调用外部回调,不再维护内部状态 onSort?.(data); }, sortDescFirst: false, state: { pagination, sorting, }, }); // 渲染部分保持不变 };
2. 缓存params,稳定queryKey
在业务组件中用useMemo缓存params,确保只有当依赖项变化时才重新生成,避免useQuery重复触发:
const params = useMemo(() => ({ order_by: buildSortByString(sorting), page_size: pagination.pageSize, }), [sorting, pagination.pageSize]);
3. 修复排序按钮逻辑
移除重复的排序触发代码,直接使用Tanstack Table提供的官方排序处理器:
{canSort && ( <SortIconTable onSort={header.column.getToggleSortingHandler()} sortDirection={header.column.getIsSorted()} /> )}
4. 稳定columns引用
在业务组件中用useMemo缓存customColumns,避免每次渲染新建数组:
const customColumns = useMemo(() => [ // 你的列定义,比如: { id: 'name', header: '名称', cell: (info) => info.row.original.name, }, // 其他列... ], []); // 若列定义依赖其他状态,添加到依赖数组中
内容的提问来源于stack exchange,提问作者Vitor
相关产品推荐
相关产品推荐

