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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 10:44:54