Next.js中Tanstack Table分页数据获取最佳实践及实现困惑
Tanstack Table 服务端分页数据获取最佳实践
核心结论
服务端处理分页(后端接收页码、页大小返回对应数据)是更优方案,尤其在数据量较大时,能减少前端内存占用、降低初始加载带宽消耗。以下是适配你现有代码的改造方案:
步骤1:调整后端接口返回格式
让你的NodeJS接口返回当前页数据和总数据条数,示例返回结构:
{ data: Trainee[], // 当前页数据数组 total: number // 全量数据总条数 }
步骤2:改造父组件(TraineeTable)
将父组件改为客户端组件,统一管理分页状态和数据请求逻辑:
'use client' import { Trainee, columns } from "./columns" import DataTable from "./data-table" import { useState, useEffect } from "react" async function getTraineeData(page: number, pageSize: number): Promise<{ data: Trainee[], total: number }> { try { // 修正URL参数格式,确保页码和页大小正确传递 const response = await fetch(`....(fetch url)?page=${page}&limit=${pageSize}`, { method: 'GET', headers: { 'Content-Type': 'application/json', }, }); if (!response.ok) { throw new Error('获取表格数据失败'); } return await response.json(); } catch (err) { console.error(err); return { data: [], total: 0 }; } } const TraineeTable = () => { const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(10); const [data, setData] = useState<Trainee[]>([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(false); // 监听分页参数变化,自动触发数据请求 useEffect(() => { const fetchData = async () => { setLoading(true); const result = await getTraineeData(pageIndex, pageSize); setData(result.data); setTotal(result.total); setLoading(false); }; fetchData(); }, [pageIndex, pageSize]); return ( <div className="container mx-auto py-10"> {loading && <div className="text-center py-4">加载中...</div>} <DataTable columns={columns} data={data} pageIndex={pageIndex} pageSize={pageSize} total={total} onPageIndexChange={setPageIndex} onPageSizeChange={setPageSize} /> </div> ) } export default TraineeTable;
步骤3:改造子组件(DataTable)
移除内部分页状态,通过props接收分页参数和回调,让表格基于后端返回的总条数计算分页逻辑:
'use client' import { ColumnDef, flexRender, useReactTable, getCoreRowModel, getPaginationRowModel, SortingState, getSortedRowModel, ColumnFiltersState, getFilteredRowModel, VisibilityState } from '@tanstack/react-table'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import * as React from "react" import { Trainee, columnTranslations } from './columns'; import { BiChevronDown } from 'react-icons/bi'; interface DataTableProps<TData, TValue> { columns: ColumnDef<TData, TValue>[] data: TData[] pageIndex: number pageSize: number total: number onPageIndexChange: (index: number) => void onPageSizeChange: (size: number) => void } const DataTable = <TData, TValue>({ columns, data, pageIndex, pageSize, total, onPageIndexChange, onPageSizeChange }: DataTableProps<TData, TValue>) => { const [sorting, setSorting] = React.useState<SortingState>([]) const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]) const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({}) const [rowSelection, setRowSelection] = React.useState({}) const table = useReactTable({ columns, data, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), // 监听分页变化,通知父组件更新状态 onPaginationChange: ({ pageIndex: newPageIndex, pageSize: newPageSize }) => { onPageIndexChange(newPageIndex); if (newPageSize !== pageSize) { onPageSizeChange(newPageSize); } }, onSortingChange: setSorting, onColumnFiltersChange: setColumnFilters, onColumnVisibilityChange: setColumnVisibility, onRowSelectionChange: setRowSelection, state: { sorting, columnFilters, columnVisibility, rowSelection, pagination: { pageIndex, pageSize, }, }, // 传入总条数,让表格计算总页数 totalRowCount: total, // 禁用前端分页,完全依赖服务端数据 manualPagination: true, }) return ( <div className='w-full'> <div className="flex items-center py-4 gap-5"> <Input placeholder="פילטור לפי שם" value={(table.getColumn("fullName")?.getFilterValue() as string) ?? ""} onChange={(event) => table.getColumn("fullName")?.setFilterValue(event.target.value) } className="max-w-sm rounded" /> <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="outline" className="ml-auto rounded"> <BiChevronDown className="ml-2 h-4 w-4" />columns </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end"> {table .getAllColumns() .filter( (column) => column.getCanHide() ) .map((column) => { return ( <DropdownMenuCheckboxItem key={column.id} className="capitalize" checked={column.getIsVisible()} onCheckedChange={(value) => column.toggleVisibility(!!value) } dir="rtl" > {columnTranslations[column.id] || column.id} </DropdownMenuCheckboxItem> ) })} </DropdownMenuContent> </DropdownMenu> </div> <div className="rounded-md border px-2"> <Table> <TableHeader> {table.getHeaderGroups().map((headerGroup) => ( <TableRow key={headerGroup.id}> {headerGroup.headers.map((header) => { return ( <TableHead key={header.id}> {header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext() )} </TableHead> ) })} </TableRow> ))} </TableHeader> <TableBody> {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( <TableRow key={row.id} data-state={row.getIsSelected() && "selected"} > {row.getVisibleCells().map((cell) => ( <TableCell key={cell.id}> {flexRender(cell.column.columnDef.cell, cell.getContext())} </TableCell> ))} </TableRow> )) ) : ( <TableRow> <TableCell colSpan={columns.length} className="h-24 text-center"> 暂无数据 </TableCell> </TableRow> )} </TableBody> </Table> </div> {table.getFilteredSelectedRowModel().rows.length > 0 && (<div className="flex-1 mt-2 text-sm text-muted-foreground"> {table.getFilteredSelectedRowModel().rows.length} of{" "} {table.getFilteredRowModel().rows.length} 条数据被选中 </div>) } <div className="flex items-center justify-center gap-5 space-x-2 py-4"> <Button variant="outline" size="sm" className="text-green2 cursor-pointer" onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()} > 上一页 </Button> <Button variant="outline" size="sm" className="text-green2 cursor-pointer" onClick={() => table.nextPage()} disabled={!table.getCanNextPage()} > 下一页 </Button> <span className="flex items-center gap-1"> <div>页码</div> <strong> {table.getState().pagination.pageIndex + 1}</strong> /{" "} <strong>{table.getPageCount()}</strong> </span> <span className="flex items-center gap-1"> 跳转到: <input type="number" defaultValue={table.getState().pagination.pageIndex + 1} onChange={(e) => { const page = e.target.value ? Number(e.target.value) - 1 : 0; table.setPageIndex(page); }} className="border p-1 rounded w-16 bg-transparent" /> </span> <select value={table.getState().pagination.pageSize} onChange={(e) => { table.setPageSize(Number(e.target.value)); }} className="p-2 bg-transparent" > {[10, 20, 30, 50].map((size) => ( <option key={size} value={size}> 显示 {size} 条 </option> ))} </select> </div> </div> ) }; export default DataTable;
关键改动说明
- 状态提升:将分页状态(pageIndex、pageSize)移到父组件,统一管理数据请求逻辑,避免子组件状态分散
- 手动分页模式:开启
manualPagination: true,告知Tanstack Table不处理前端分页,完全依赖服务端返回数据 - 总条数传入:通过
totalRowCount传递后端返回的总数据量,让表格正确计算总页数和分页按钮状态 - 分页回调:通过
onPaginationChange监听表格分页操作,通知父组件更新状态并触发新的数据请求
内容的提问来源于stack exchange,提问作者steve123
相关产品推荐
相关产品推荐

