PAYATHON 2026

Shadcn/UI 的 DataTablePagination 为何在父组件更新后不刷新?

数据小陈

父组件已经配置了受控分页,代码中没有明显遗漏 state.pagination 或 onPaginationChange。表格行更新了,拆出的分页组件却停在旧状态,建议先检查组件是否被缓存。如果项目启用了 React Compiler,还需要验证它与 useReactTable 返回对象的兼容性。

目前缺少 React、TanStack Table 的版本和编译配置,还不能直接认定 React Compiler 是根因。将代码移回父组件或加入 useRef 后问题消失,都只能作为排查线索。

为什么传入同一个 table 可能让界面停在旧状态?

在题目所用的 useReactTable 这类 API 中,table 对象可以保持相同引用,而其方法读取到的内部状态已经变化。例如,分页后 table.getState().pagination.pageIndex 可以改变,但传给子组件的 table 仍是同一个对象。React 官方的兼容性检查文档将 useReactTable 列为具有内部可变状态的 API 示例。React:incompatible-library

如果 DataTablePagination 被 React.memo 包装,或者 React Compiler 根据稳定的 table 引用复用了子组件或方法调用结果,页码和按钮状态就可能停留在上一次渲染的值。这与题目描述的现象相符,但仍需结合项目配置验证。

普通的、未被缓存的函数子组件通常会随父组件重新渲染。table 引用没有变化,本身不足以解释子组件为什么不更新,还需要找到实际存在的缓存机制。

第一步:确认父组件的分页状态确实变化

先在父组件里、分页组件旁边临时显示受控状态:

<output>
  pageIndex: {pagination.pageIndex}, pageSize: {pagination.pageSize}
</output>
<DataTablePagination table={table} />

点击下一页,观察 pageIndex 是否从 0 变为 1。

  • 如果父组件显示的值变了,但分页组件仍显示第 1 页,优先检查子组件缓存。
  • 如果值没有变化,检查实际运行代码是否仍将 pagination 传入 state,并将 setPagination 传入 onPaginationChange。
  • 如果值先变化又回到 0,检查组件是否重新挂载,以及数据、筛选等变化是否触发了分页重置。

题目已经有下面这组配置,不需要再增加一套分页状态:

const [pagination, setPagination] = React.useState<PaginationState>({
  pageIndex: 0,
  pageSize: 10,
})

// useReactTable 的相关配置
onPaginationChange: setPagination,
state: {
  sorting,
  columnFilters,
  columnVisibility,
  pagination,
},

第二步:验证是否与 React Compiler 有关

检查构建配置是否启用了 React Compiler,例如是否配置了 babel-plugin-react-compiler。安装 React 本身不等于启用了 React Compiler。

如果确实启用了,可以在原有 DataTablePagination 函数体的第一条语句位置加入 "use no memo",保留其余实现,再移除渲染计数代码进行验证。下面是一个简化后的测试组件:

import type { Table } from "@tanstack/react-table"
import { Button } from "@/components/ui/button"

interface DataTablePaginationProps<TData> {
  table: Table<TData>
}

export function DataTablePagination<TData>({
  table,
}: DataTablePaginationProps<TData>) {
  "use no memo";

  const { pageIndex } = table.getState().pagination

  return (
    <div className="flex items-center gap-2">
      <Button
        type="button"
        variant="outline"
        onClick={() => table.previousPage()}
        disabled={!table.getCanPreviousPage()}
      >
        上一页
      </Button>

      <span>
        第 {pageIndex + 1} 页,共 {table.getPageCount()} 页
      </span>

      <Button
        type="button"
        variant="outline"
        onClick={() => table.nextPage()}
        disabled={!table.getCanNextPage()}
      >
        下一页
      </Button>
    </div>
  )
}

"use no memo" 会让 React Compiler 跳过该函数的优化,必须放在函数体开头。React 将它定位为调试或兼容性处理的临时出口。React:use no memo

如果加入后恢复正常,可以进一步排查编译优化与表格状态读取之间的问题。若仍不更新,可临时让调用 useReactTable 的父组件也退出编译,以区分父组件中的缓存和子组件中的缓存。只在父组件上添加指令,并不意味着独立定义的子组件也自动退出编译。

同时检查实际导出或包装代码中是否有 React.memo(DataTablePagination),以及是否有仅依赖 [table] 的 useMemo 缓存分页 JSX 或状态值。"use no memo" 不会移除手写的 React.memo 或 useMemo。

保留组件拆分时,可以显式传递分页值

也可以让分页组件直接接收页码、总页数和按钮状态,这样更容易追踪数据流。分页变化会体现在 props 上,子组件不必通过稳定的 table 对象读取隐藏的变化。

下面是一个精简的独立分页组件:

import { Button } from "@/components/ui/button"

interface PaginationControlsProps {
  pageIndex: number
  pageCount: number
  canPreviousPage: boolean
  canNextPage: boolean
  onPreviousPage: () => void
  onNextPage: () => void
}

export function PaginationControls({
  pageIndex,
  pageCount,
  canPreviousPage,
  canNextPage,
  onPreviousPage,
  onNextPage,
}: PaginationControlsProps) {
  return (
    <div className="flex items-center gap-2">
      <Button
        type="button"
        variant="outline"
        disabled={!canPreviousPage}
        onClick={onPreviousPage}
      >
        上一页
      </Button>

      <span>
        第 {pageCount === 0 ? 0 : pageIndex + 1} 页,共 {pageCount} 页
      </span>

      <Button
        type="button"
        variant="outline"
        disabled={!canNextPage}
        onClick={onNextPage}
      >
        下一页
      </Button>
    </div>
  )
}

在父组件中替换原来的分页组件调用:

<PaginationControls
  pageIndex={pagination.pageIndex}
  pageCount={table.getPageCount()}
  canPreviousPage={table.getCanPreviousPage()}
  canNextPage={table.getCanNextPage()}
  onPreviousPage={() => table.previousPage()}
  onNextPage={() => table.nextPage()}
/>

这里直接使用已有的 pagination.pageIndex,不需要在子组件里再创建一份分页状态。若已经确认父组件中的表格方法调用也受到编译缓存影响,应先让该父组件退出相关优化,确保传下去的值是最新的。

原组件还显示每页行数、选中行数和筛选后行数。保留这些功能时,也应传入它们实际依赖的值及对应回调。如果只额外传一个 pageIndex,继续通过可能被缓存的 table 方法读取其他状态,就无法覆盖所有更新场景。

为什么 useRef 渲染计数不能作为修复?

修改 ref.current 不会触发 React 重新渲染,这段计数代码本身不是一种刷新机制。

加入计数代码后问题暂时消失,可能是因为它改变了编译器对函数的分析或缓存方式。仅凭这个现象,无法确定具体是哪一处优化发生了变化,也不能保证构建环境变化后仍然有效。计数和日志适合辅助观察,不应成为分页组件正常工作的条件。

同样,将整个分页 JSX 移回父组件,可能只是让状态读取避开了原先的缓存位置,不能由此认定组件拆分本身有问题。

修复后检查哪些行为?

使用至少两页的数据验证:点击下一页后,行内容、页码和上一页按钮同步变化;返回第一页后,上一页按钮禁用;到达末页后,下一页按钮禁用;改变每页行数后,下拉框值和总页数同步变化。

如果项目没有启用 React Compiler,也没有手写缓存,需要继续检查实际组件包装、重新挂载和状态重置。现有材料没有证据将原因归到 Laravel 或 Inertia.js。

备注:内容仅供参考。