资讯动态

TanStack Solid-Table 分页实战指南:客户端与手动服务端分页的完整实现方案

发布时间:2026/9/21 15:45:32 来源:尧图企业网站定制
TanStack Solid-Table 分页实战指南客户端与手动服务端分页的完整实现方案【免费下载链接】table Headless UI for building powerful tables datagrids for TS/JS - React-Table, Vue-Table, Solid-Table, Svelte-Table项目地址: https://gitcode.com/gh_mirrors/ta/table分页是数据表格最基础也最常用的交互能力。TanStack Table 在tanstack/solid-table中通过rowPaginationFeature特性与可插拔的行模型Row Model提供了对客户端分页和服务端分页的完整支持。本文将以 Solid 框架为视角带你掌握从特性装配、分页状态管理到分页按钮与信息 API 的完整链路并深入仓库源码剖析manualPagination、pageCount、rowCount、autoResetPageIndex等关键选项的底层语义最终给出与 TanStack Query 集成的真实生产模式。一、Pagination 特性在 Solid-Table 中的定位TanStack Table 采用 headless 设计分页能力被封装为可组合的特性Feature与行模型。在tanstack/solid-table中分页相关的核心构件有三个rowPaginationFeature注册分页状态、选项与全部分页 APIcreatePaginatedRowModel()客户端分页的行模型工厂负责对上游行模型做切片slicetableFeatures()特性组合函数将各特性与行模型装配为一张表的完整配置。从源码看createPaginatedRowModel位于 packages/table-core/src/features/row-pagination/createPaginatedRowModel.ts它通过tableMemo建立了一个以getPrePaginatedRowModel()、atoms.pagination以及默认情况下atoms.expanded为依赖的记忆化工厂——也就是说只要上游行模型或分页状态发生变化分页结果才会被重新计算。这保证了在 Solid 的细粒度响应式模型下翻页、过滤、排序等操作只触发必要的重算。装配方式如下import { createTable, tableFeatures, rowPaginationFeature, createPaginatedRowModel, } from tanstack/solid-table const features tableFeatures({ rowPaginationFeature, paginatedRowModel: createPaginatedRowModel(), // 客户端分页行模型 // manualPagination: true, // 手动服务端分页时开启 }) const table createTable({ features, columns, get data() { return data() }, })注意两点其一行模型槽位slot是类型检查的一部分加入rowPaginationFeature后如果需要客户端分页就应把paginatedRowModel放在该特性之后注册其二Solid 中传入createTable的响应式输入如data应当使用 getter 形式即get data() { return data() }这样才能让表格实例跟随 Solid 信号更新。在 examples/solid/pagination/src/App.tsx 的官方示例中还提供了Regenerate Data与Stress Test (1M rows)两个按钮用于验证分页切片在 100 万行数据下的表现读者可直接运行该示例体验客户端分页的能力边界。二、客户端分页何时使用以及它的行模型客户端分页意味着你一次取回的表数据包含全部行表格实例在前端自行完成切片逻辑。这是最简单直接的方案适合浏览器能够完整获取并驻留数据集的场景当全量数据集在查询、传输或存储层面代价过高时则应转向服务端分页。需要强调的是决定边界的不只是行数还涉及数据获取成本、传输体积、内存占用以及过滤/排序与分页的一致性维护完整的决策框架见 Client-Side vs Server-Side Guide。分页与虚拟化是两回事虚拟化Virtualization / Windowing只减少渲染工作——它只挂载可见行但数据仍然全部驻留在浏览器中。因此虚拟化可以配合客户端或服务端分页使用但无法替代服务端处理当完整数据集大到无法加载时你需要的仍是服务端分页。这一渲染是独立决策的区分详见 Client-Side vs Server-Side Guide。Pagination Row Model 的切片原理使用内置客户端分页只需装配rowPaginationFeature与createPaginatedRowModel()import { createTable, tableFeatures, rowPaginationFeature, createPaginatedRowModel, } from tanstack/solid-table const features tableFeatures({ rowPaginationFeature, paginatedRowModel: createPaginatedRowModel(), }) const table createTable({ features, columns, data, })查看 createPaginatedRowModel.ts 的实现可以发现切片逻辑非常直白const { pageSize, pageIndex } pagination ?? getDefaultPaginationState() let paginatedRows rows if (pageSize ! Infinity || pageIndex ! 0) { const pageStart pageSize * pageIndex const pageEnd pageStart pageSize paginatedRows rows.slice(pageStart, pageEnd) }值得注意的细节当pageSize为Infinity且pageIndex为 0 时跳过切片即Show All场景默认不展开子行paginateExpandedRows为 false 时分页后会用expandRows把当前页内行的子行内联展开并通过seenFlatRows集合去重保证flatRows中每行只出现一次因此分页发生在过滤、排序、分组之后即getPrePaginatedRowModel()的输出之上。三、手动服务端分页当数据量超出前端承载能力时你需要自己控制分页逻辑服务端返回当前页的数据表格只负责展示与状态管理。核心选项manualPagination、pageCount、rowCount服务端分页不需要分页行模型但如果你在共享组件中为其他表提供了它仍可通过manualPagination: true关闭客户端切片。该选项会让表格实例在底层改用table.getPrePaginatedRowModel并假定传入的data已经是按页切好的数据。表格实例本身无法得知后端的行数/页数必须由你告知两种方式二选一rowCount传入总行数表格内部依据rowCount与当前pageSize计算pageCountMath.ceil(rowCount / pageSize)pageCount如果你已经知道总页数可直接传入不知道时传-1。pageCount: -1未知页数时的行为差异可以从 rowPaginationFeature.utils.ts 的源码中精确读出判断 APIpageCount 为 -1未知时的行为源码依据getCanNextPage()恒为true表格无法探测到末尾pageCount -1 → return truegetCanPreviousPage()取决于当前pageIndex是否大于 0pageIndex 0getCanLastPage()恒为false不存在有限的末页要求Number.isFinite(pageCount)配置示例import { createTable, tableFeatures, rowPaginationFeature, } from tanstack/solid-table const features tableFeatures({ rowPaginationFeature }) const table createTable({ features, columns, data, manualPagination: true, // 关闭客户端分页 rowCount: dataQuery.data?.rowCount, // 传入总行数pageCount 内部计算 // pageCount: dataQuery.data?.pageCount, // 或直接传入总页数 })[!NOTE]manualPagination: true会让表格实例假定传入的data已经是按页切好的数据。在 Solid 中处理响应式服务端数据与 React 版本不同Solid 的createTable通过 getter 建立响应式绑定。服务端返回的rowCount也应通过 getter 传入const table createTable({ features, columns, get data() { return dataQuery.data?.rows ?? [] }, get rowCount() { return dataQuery.data?.rowCount }, manualPagination: true, })这样当查询结果更新时表格会自动读取最新的行与总行数无需手动同步。配合 TanStack Query 的两种服务端模式TanStack Query 负责请求生命周期TanStack Table 负责受控的分页状态。完整示例见 examples/solid/with-tanstack-query。模式一基于页码page-index的useQuery将分页状态纳入查询键接口返回当前页行与总rowCount二者都交给表格const dataQuery useQuery(() ({ queryKey: [people, offset, pagination(), sorting(), globalFilter()], queryFn: () fetchPeople({ pagination: pagination(), sorting: sorting(), globalFilter: globalFilter(), }), placeholderData: keepPreviousData, })) const table createTable({ features, columns, get data() { return dataQuery.data?.rows ?? [] }, get rowCount() { return dataQuery.data?.rowCount }, state: { get pagination() { return pagination() }, }, onPaginationChange: setPagination, manualPagination: true, })因为总行数已知这种模式完整支持页数显示、页码跳转与lastPage()。模式二基于游标cursor的useInfiniteQuery游标 API 返回当前行、nextCursor与hasNextPage当 ID 唯一且服务端排序稳定时游标可以直接取最后一行 IDconst dataQuery useInfiniteQuery(() ({ queryKey: [ people, cursor, pagination().pageSize, sorting(), globalFilter(), ], queryFn: ({ pageParam }) fetchPeople({ cursor: pageParam, pageSize: pagination().pageSize, sorting: sorting(), globalFilter: globalFilter(), }), initialPageParam: null, getNextPageParam: (lastPage) lastPage.nextCursor, })) const currentPage () dataQuery.data?.pages[pagination().pageIndex] const canNextPage () Boolean(dataQuery.data?.pages[pagination().pageIndex 1]) || Boolean(currentPage()?.hasNextPage) const table createTable({ features, columns, get data() { return currentPage()?.rows ?? [] }, pageCount: -1, state: { get pagination() { return pagination() }, }, onPaginationChange: setPagination, manualPagination: true, }) async function goToNextPage() { const nextPageIndex pagination().pageIndex 1 if (!dataQuery.data?.pages[nextPageIndex]) { const result await dataQuery.fetchNextPage() if (!result.data?.pages[nextPageIndex]) return } table.nextPage() }游标模式的三个关键实践Next 按钮使用canNextPage()因为页数未知getCanNextPage()无法判断是否到达末尾已缓存的页支持向后导航useInfiniteQuery会缓存各页往前翻无需重新请求不存在有限末页getCanLastPage()返回false不要显示末页按钮排序、过滤或页大小变化时应手动把pageIndex重置为 0。四、分页状态Solid 信号、外部 Atom 与受控模式的取舍无论客户端还是服务端分页都可使用内置的pagination状态与 API。pagination状态是一个包含两个字段的对象pageIndex当前页索引从 0 开始pageSize当前每页行数。在 Solid 中表格状态原子由 Solid 信号支撑因此table.atoms.pagination.get()在被跟踪的作用域JSX、createMemo、createEffect或table.Subscribe内是响应式读取在事件处理器中同样的调用则直接返回当前值。createTable的响应式接线可见 packages/solid-table/src/createTable.ts它通过mergeProps合并选项并在createComputed中把用户选项重新同步回表格实例同时用onCleanup在组件卸载时清理响应式订阅。方式一v9 推荐外部 Atom当需要在表格之外读取pagination最常见的是拼服务端查询键时推荐用外部 atom 通过atoms选项交给表格。atom 保持细粒度订阅查询键不依赖组件局部状态import { createAtom, useSelector } from tanstack/solid-store import { createTable, tableFeatures, rowPaginationFeature, createPaginatedRowModel, type PaginationState, } from tanstack/solid-table const features tableFeatures({ rowPaginationFeature }) const paginationAtom createAtomPaginationState({ pageIndex: 0, // 初始页索引 pageSize: 10, // 默认每页行数 }) // 在任何需要取值的地方订阅例如查询键 const pagination useSelector(paginationAtom) const table createTable({ features, columns, data, atoms: { pagination: paginationAtom, // 表格分页 API 将直接更新该 atom }, })方式二v8 风格state onPaginationChangestate.pagination配合onPaginationChange的模式仍然受支持适合简单集成或 v8 迁移场景但它不如外部 atom 细粒度const [pagination, setPagination] createSignalPaginationState({ pageIndex: 0, // 初始页索引 pageSize: 10, // 默认每页行数 }) const table createTable({ features, columns, data, state: { get pagination() { return pagination() // 将信号接回表格 }, }, onPaginationChange: setPagination, })方式三initialState 只设初值如果你不需要在外部作用域管理分页状态只想改初始pageIndex与pageSize使用initialStateconst table createTable({ features, columns, data, initialState: { pagination: { pageIndex: 2, // 自定义初始页索引 pageSize: 25, // 自定义默认每页行数 }, }, })[!NOTE] 不要在atoms、state、initialState中同时提供pagination切片。受控值atoms或state会覆盖initialState三者只能选其一。更深入的对比见 Table State Guide。五、Pagination 选项深度解析除了服务端分页用到的manualPagination、pageCount、rowCount还有几个选项需要理解。autoResetPageIndex自动重置页索引默认情况下当客户端行模型重算如data更新、过滤变化、排序变化、分组变化时pageIndex会被重置为 0。manualPagination: true时该行为自动关闭但你可以显式给autoResetPageIndex赋布尔值覆盖它全局的autoResetAll可一次性关闭/开启所有特性的自动重置。const table createTable({ features, columns, data, autoResetPageIndex: false, // 关闭 pageIndex 自动重置 // autoResetAll: false, // 或一次性关闭所有自动重置 })一个常见的使用场景是边编辑边查看如行内编辑每次编辑都会更新data并触发行模型重算若默认重置用户会被瞬间拉回第一页设为静态false可以在重算后保持当前页。如果同时使用展开expanding特性建议配合同样的autoResetExpanded: false避免编辑时展开行被折叠。需要警惕的是关闭autoResetPageIndex后你要自己处理页索引重置逻辑否则可能出现当前页已无数据的空页。从源码看自动重置的实际判定逻辑位于 rowPaginationFeature.utils.ts 的table_autoResetPageIndexif ( table.options.autoResetAll ?? table.options.autoResetPageIndex ?? !table.options.manualPagination ) { // 已在首页则跳过避免无谓的 onPaginationChange 副作用 const currentPageIndex table.atoms.pagination?.get()?.pageIndex ?? defaultPageIndex if (currentPageIndex defaultPageIndex) return table_resetPageIndex(table, true) }实现还包含一个巧妙的优化若当前已经在首页直接跳过重置避免把无操作路由到onPaginationChange而引发多余的请求副作用如每次过滤都重新拉取数据。[!NOTE] 自动重置只在被包含的客户端行模型重算时触发。如果手动服务端分页的表未包含过滤、排序、分组等相关行模型改变这些受控状态不会触发页索引重置即使autoResetPageIndex或autoResetAll为true。此时应在对应 change handler 中自行重置pageIndex。其他服务端选项的源码语义pageCount优先table_getPageCount中options.pageCount非空即直接返回否则由rowCount / pageSize计算rowPaginationFeature.utils.tsrowCount优先table_getRowCount中options.rowCount存在即使用否则统计getPrePaginatedRowModel().rows.length同文件 L432-L437因此客户端模式下过滤、分组、排序、展开都反映在页数计算中setPageSize会保持当前顶部行在视野内新页大小被钳制到至少 1pageIndex按topRowIndex / pageSize重新计算同文件 L198-L215setPageIndex会钳制范围未知页数undefined或-1时允许任意非负索引已知页数时钳制在0 ~ pageCount - 1之间同文件 L117-L137。六、Pagination APIs按钮与信息rowPaginationFeature提供了一组分页实例 API用于搭建分页 UI。类型签名见 rowPaginationFeature.types.ts。分页按钮 APIAPI用途getCanPreviousPage在第一页时禁用上一页按钮getCanNextPage没有更多页时禁用下一页按钮getCanLastPage未知有限末页时禁用末页按钮previousPage前往上一页按钮点击处理器nextPage前往下一页按钮点击处理器firstPage前往第一页按钮点击处理器lastPage前往最后一页按钮点击处理器setPageIndex跳转到某页输入框resetPageIndex重置页索引到初始值setPageSize每页行数输入/下拉框resetPageSize重置页大小到初始值setPagination一次性设置整个分页状态resetPagination重置到初始分页状态[!NOTE] 这些分页 API 仅在启用rowPaginationFeature时可用。在 Solid 中结合For组件与 atom 读取渲染分页控件button onClick{() table.firstPage()} disabled{!table.getCanPreviousPage()} {} /button button onClick{() table.previousPage()} disabled{!table.getCanPreviousPage()} {} /button button onClick{() table.nextPage()} disabled{!table.getCanNextPage()} {} /button button onClick{() table.lastPage()} disabled{!table.getCanLastPage()} {} /button select value{table.atoms.pagination.get().pageSize} onChange{e { table.setPageSize(Number(e.currentTarget.value)) }} For each{[10, 20, 30, 40, 50]} {pageSize option value{pageSize}Show {pageSize}/option} /For /select分页信息 APIgetPageCount显示总页数getRowCount显示总行数。这两者的取值优先级在第五节已有源码级说明getPageCount优先取options.pageCount否则由rowCount与pageSize计算getRowCount优先取options.rowCount否则统计分页前的行模型行数。在官方示例 examples/solid/pagination/src/App.tsx 中还展示了两种信息型 UIPage {pageIndex 1} of {getPageCount()}的页码指示以及Showing {rows.length} of {getRowCount()} Rows的行数统计同时提供了跳转到页数字输入框setPageIndex和 Show AllpageSize: Infinity选项——Infinity页大小在PaginationState类型中被明确允许用于把所有行放在单页。七、小结与选型建议综合文档与源码选择分页方案时可遵循以下路径数据量可控、浏览器能驻留全量数据使用客户端分页装配rowPaginationFeature createPaginatedRowModel()配合autoResetPageIndex控制过滤/排序/编辑后的行为全量数据过大需要服务端处理使用manualPagination: true按接口形态在rowCount/pageCount或游标模式中二选一与 TanStack Query 的useQuery/useInfiniteQuery集成分页状态需要在表格外共享如查询键优先用外部 atom 通过atoms选项注入获得细粒度响应式订阅简单场景可用state onPaginationChange受控模式仅设初值则用initialState渲染性能仍是独立问题无论哪种分页行数巨大时都应评估虚拟化但它不能替代服务端分页。通过rowPaginationFeature的源码rowPaginationFeature.utils.ts 与 rowPaginationFeature.types.ts你可以确认每个选项与 API 的精确语义通过 examples/solid/pagination 与 examples/solid/with-tanstack-query 两个示例则可以获得可直接运行、可复制的完整实现。【免费下载链接】table Headless UI for building powerful tables datagrids for TS/JS - React-Table, Vue-Table, Solid-Table, Svelte-Table项目地址: https://gitcode.com/gh_mirrors/ta/table创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价