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

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 ?? []}
/>

根源排查

  1. 双状态冲突:Table组件内部维护了sorting和pagination状态,同时外部业务组件也维护了一份相同逻辑的状态。当内部状态更新时触发外部回调更新业务组件状态,业务组件状态变化又会重新渲染Table组件,导致循环更新。
  2. 不稳定的queryKey:业务组件中params是每次渲染都新建的对象,导致useQuery的queryKey每次都变化,重复触发数据请求。请求完成后更新rows,触发Table组件重新渲染,进而再次触发状态更新。
  3. 重复的排序触发逻辑:Table组件中排序按钮同时调用了table.setSorting和header.column.getToggleSortingHandler(),导致两次触发排序状态更新,加剧循环。
  4. 不稳定的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 16:15:55