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

Tanstack React Table数据更新后Table对象未更新的问题排查

问题:TanStack React Table数据更新后子组件不重新渲染

使用TanStack React Table时,数据更新后Table对象未同步触发子组件<SageTableBody>重新渲染,核心代码如下:

import { useQuery } from "@tanstack/react-query";
import { createColumnHelper, flexRender, Table } from "@tanstack/react-table";
import {
  useReactTable,
  getCoreRowModel as defaultGetCoreRowModel,
} from "@tanstack/react-table";
import { StrictMode, useMemo, memo } from "react";
import { createRoot } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();

const columnHelper = createColumnHelper<User>();

interface User {
  id: number;
  name: string;
  age: number;
  email: string;
  status: "active" | "inactive";
  createdAt: string;
}

const fetchData = async (): Promise<User[]> => {
  return new Promise((resolve) => {
    setTimeout(() => {
      const users: User[] = Array.from({ length: 10 }, (_, i) => ({
        id: i + 1,
        name: `User ${i + 1}`,
        age: Math.floor(Math.random() * 60) + 18,
        email: `user${i + 1}@example.com`,
        status: Math.random() > 0.5 ? "active" : "inactive",
        createdAt: new Date(
          Date.now() - Math.floor(Math.random() * 10000000000)
        ).toISOString(),
      }));
      resolve(users);
    }, 3000);
  });
};

const columns = [
  columnHelper.accessor("id", {
    header: "ID",
    footer: (info) => info.column.id,
    size: 40,
  }),
  columnHelper.accessor("name", {
    header: "Name",
    footer: (info) => info.column.id,
    size: 100,
  }),
  columnHelper.accessor("age", {
    header: "Age",
    footer: (info) => info.column.id,
    size: 40,
  }),
  columnHelper.accessor("email", {
    header: "Email",
    footer: (info) => info.column.id,
    size: 200,
  }),
  columnHelper.accessor("status", {
    header: "Status",
    footer: (info) => info.column.id,
    size: 60,
  }),
  columnHelper.accessor("createdAt", {
    header: "Created At",
    cell: (info) => new Date(info.getValue()).toLocaleDateString(),
    footer: (info) => info.column.id,
    size: 100,
  }),
];

const App = () => {
  const { data, isLoading } = useQuery({
    queryKey: ["fetch"],
    queryFn: async () => {
      return fetchData();
    },
    initialData: [],
  });

  return <SageTable data={data} />;
};

interface SageTableProps {
  data: User[];
}

const SageTable = memo(({ data }: SageTableProps) => {
  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: defaultGetCoreRowModel(),
    columnResizeMode: "onChange",
  });

  const columnSizeVars = useMemo(() => {
    const headers = table.getFlatHeaders();
    const colSizes: { [key: string]: number } = {};
    for (let i = 0; i < headers.length; i++) {
      const header = headers[i]!;
      colSizes[`--header-${header.id}-size`] = header.getSize();
      colSizes[`--col-${header.column.id}-size`] = header.column.getSize();
    }
    return colSizes;
  }, [table.getState().columnSizingInfo, table.getState().columnSizing]);

  return (
    <div
      className="table"
      style={{
        ...columnSizeVars,
        width: table.getTotalSize(),
      }}
    >
      <MemoizedTableBody table={table} />
    </div>
  );
});

interface SageTableBodyProps {
  table: Table<User>;
}

const SageTableBody = ({ table }: SageTableBodyProps) => {
  return (
    <div>
      {table.getRowModel().rows.map((row) => (
        <div
          className="table__body__item"
          style={{ display: "flex" }}
          key={row.id}
        >
          {row.getVisibleCells().map((cell) => (
            <div
              key={cell.id}
              style={{
                border: "1px solid gray",
                width: `calc(var(--col-${cell.column.id}-size) * 1px)`,
              }}
            >
              {flexRender(cell.column.columnDef.cell, cell.getContext())}
            </div>
          ))}
        </div>
      ))}
    </div>
  );
};

const MemoizedTableBody = memo(
  SageTableBody,
  (prev, next) => prev.table.options.data === next.table.options.data
) as typeof SageTableBody;

const rootElement = document.getElementById("root");
if (!rootElement) throw new Error("Failed to find the root element");

createRoot(rootElement).render(
  <StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </StrictMode>
);

根据官方文档描述:

当data选项的引用发生变化(通过Object.is比较)时,表格会重新处理数据,依赖核心数据模型的各类处理(如分组、排序、筛选等)也会重新执行。

但实际中table.options.data的引用未触发预期变化,导致子组件不重新渲染,且Table对象没有提供setData方法。


解决方案

1. 移除自定义memo比较函数(最简方案)

问题根源在于自定义的memo比较函数逻辑不可靠,table.options.data不能准确反映数据更新状态。直接使用memo默认的浅比较即可:

const MemoizedTableBody = memo(SageTableBody) as typeof SageTableBody;

当data更新时,useReactTable会返回全新的table对象,默认浅比较会检测到props变化,触发子组件重新渲染。

2. 基于data引用的自定义比较(保留性能优化)

如果需要手动控制渲染时机,可以将data作为props传递给子组件,直接比较data的引用:

// 在SageTable中修改传递的props
<MemoizedTableBody table={table} data={data} />

// 修改MemoizedTableBody的定义和比较函数
interface SageTableBodyProps {
  table: Table<User>;
  data: User[];
}

const MemoizedTableBody = memo(
  SageTableBody,
  (prev, next) => prev.data === next.data
) as typeof SageTableBody;

3. 基于核心数据模型的比较

另一种可靠方式是比较表格的核心数据模型(如rowModel),因为数据更新时rowModel会重新生成:

const MemoizedTableBody = memo(
  SageTableBody,
  (prev, next) => {
    return prev.table.getRowModel().rows === next.table.getRowModel().rows;
  }
) as typeof SageTableBody;

内容的提问来源于stack exchange,提问作者SageJustus

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 00:24:54