使用React Table与Django实现16万行大表分页的最佳方案是什么?
我有一张包含16万行、20+列的表,目前我已经配置了自定义Django Rest Framework分页,可通过查询字符串设置每页条数和页码:http://127.0.0.1:8000/api/ix/?page=2&page_size=100
返回的JSON格式如下:
{ "count": 166710, "next": "http://127.0.0.1:8000/api/ix/?page=3&page_size=100", "previous": "http://127.0.0.1:8000/api/ix/?page_size=100", "results": [{...}] }
前端我打算用React Query拉取JSON数据,用React Table展示并处理分页逻辑。
从React Table的官方示例来看,它是拉取全量未分页数据后,在客户端做切片处理:
const startRow = pageSize * pageIndex const endRow = startRow + pageSize setData(serverData.slice(startRow, endRow))
可选方案
- 关闭服务端分页,按示例在客户端做数据切片
- 优势:只需少量修改示例代码即可实现
- 劣势:首次数据加载耗时长,每次分页操作都可能触发数据重加载
- 保留服务端自定义分页,自行实现分页逻辑对接
- 优势:数据已在服务端分页,加载效率更高
- 劣势:需要编写更多代码,易出问题,同时需遵循React单向数据流规则重新设计组件
- 同时使用服务端分页+客户端分页
- 该方案理论上最优,但如果服务端返回固定页长的分页数据,我不知道该如何实现。假设我无法再通过
page_size参数查询,当用户修改每页条数时,客户端该如何拉取数据?比如我的API每页返回100条数据,现在用户要展示500条,React Table会“自动”多次调用API直到凑够500条数据吗?
- 该方案理论上最优,但如果服务端返回固定页长的分页数据,我不知道该如何实现。假设我无法再通过
请问哪种方案是最优选择?
我的实现方案
最终我通过服务端分页结合多个useEffect钩子将页码、每页条数同步到URL查询参数实现了需求,对接的是同样为服务端分页的Pokemon API,和我的场景非常相似,以下是实现代码:
import React from "react"; import { useTable, usePagination } from "react-table"; import { useQuery } from "react-query"; import { fetchIXData, ColName } from "fetchData"; const fetchIXData = async (page, pageSize) => { try { const response = await fetch( `http://127.0.0.1:8000/api/ix/?page=${page + 1}&page_size=${pageSize}` ); // 我的API是1起始索引,React Table是0起始索引,所以+1 const data = await response.json(); return data; } catch (e) { throw new Error(`API error: !{e?.message}`); } }; const ColName = { col: "col1", // 其余19个列配置 }; const columns = Object.keys(ColName).map((key) => { return { Header: ColName[key], accessor: key, }; }); const initialState = { queryPageIndex: 0, queryPageSize: 20, totalCount: null, }; const PAGE_CHANGED = "PAGE_CHANGED"; const PAGE_SIZE_CHANGED = "PAGE_SIZE_CHANGED"; const TOTAL_COUNT_CHANGED = "TOTAL_COUNT_CHANGED"; const reducer = (state, { type, payload }) => { switch (type) { case PAGE_CHANGED: return { ...state, queryPageIndex: payload, }; case PAGE_SIZE_CHANGED: return { ...state, queryPageSize: payload, }; case TOTAL_COUNT_CHANGED: return { ...state, totalCount: payload, }; default: throw new Error(`未处理的动作类型: ${type}`); } }; function Table() { const [{ queryPageIndex, queryPageSize, totalCount }, dispatch] = React.useReducer(reducer, initialState); const { isLoading, error, data, isSuccess } = useQuery( ["ixData", queryPageIndex, queryPageSize], () => fetchIXData(queryPageIndex, queryPageSize), { keepPreviousData: true, staleTime: Infinity, } ); const { getTableProps, getTableBodyProps, headerGroups, prepareRow, page, canPreviousPage, canNextPage, pageOptions, pageCount, gotoPage, nextPage, previousPage, setPageSize, // 从实例获取状态 state: { pageIndex, pageSize }, } = useTable( { columns, data: isSuccess ? data.results : [], initialState: { pageIndex: queryPageIndex, pageSize: queryPageSize, }, manualPagination: true, pageCount: isSuccess ? Math.ceil(totalCount / queryPageSize) : null, }, usePagination ); React.useEffect(() => { dispatch({ type: PAGE_CHANGED, payload: pageIndex }); }, [pageIndex]); React.useEffect(() => { dispatch({ type: PAGE_SIZE_CHANGED, payload: pageSize }); gotoPage(1); }, [pageSize, gotoPage]); React.useEffect(() => { if (data?.count) { dispatch({ type: TOTAL_COUNT_CHANGED, payload: data.count, }); } }, [data?.count]); if (error) { return <p>加载出错</p>; } if (isLoading) { return <p>加载中...</p>; } return ( <> {isSuccess ? ( <> <div className="pagination"> <button onClick={() => gotoPage(0)} disabled={!canPreviousPage}> {"<<"} </button> <button onClick={() => previousPage()} disabled={!canPreviousPage}> {"<"} </button> <button onClick={() => nextPage()} disabled={!canNextPage}> {">"} </button> <button onClick={() => gotoPage(pageCount - 1)} disabled={!canNextPage} > {">>"} </button> <span> 第 <strong> {pageIndex + 1} / {pageOptions.length} </strong> 页 </span> <span> | 跳转到: <input type="number" onChange={(e) => { const page = Number(e.target.value) - 1; if (e.target.value != null) gotoPage(page); }} style={{ width: "100px" }} /> </span> <select value={pageSize} onChange={(e) => { setPageSize(Number(e.target.value)); }} > {[20, 30, 40, 50].map((pageSize) => ( <option key={pageSize} value={pageSize}> 每页显示{pageSize}条 </option> ))} </select> </div> <table {...getTableProps()}> <thead> {headerGroups.map((headerGroup) => ( <tr {...headerGroup.getHeaderGroupProps()}> {headerGroup.headers.map((column) => ( <th {...column.getHeaderProps()}> {column.render("Header")} </th> ))} </tr> ))} </thead> <tbody {...getTableBodyProps()}> {page.map((row) => { prepareRow(row); return ( <tr {...row.getRowProps()}> {row.cells.map((cell) => ( <td {...cell.getCellProps()}>{cell.render("Cell")}</td> ))} </tr> ); })} </tbody> </table> </> ) : null} </> ); } export default Table;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
内容的提问来源于stack exchange,提问作者PipEvangelist
相关产品推荐
相关产品推荐

