手把手实现高性能React分页组件的实战经验分享
谁更灵活?谁更省事?
分页组件这玩意儿,看起来简单,写起来真能让你半夜改完第三版还发现“上一页”在第一页时居然没禁用。我做过不下二十个带分页的项目,从纯静态表格到实时搜索+无限滚动混合场景,踩过的坑足够堆成个小山包。这次不讲理论,就聊三个我实际用过、线上跑过半年以上的方案:手搓原生(React + useState)、Ant Design 的 Pagination、以及一个轻量但有点野路子的自定义 Hook 方案(usePagination)。不吹不黑,哪个好用、哪个反人类,咱一条条说。
我一般先手搓,但只限小项目
小需求、内部系统、或者只是临时 demo,我第一反应就是自己写——因为快,可控,不引包。核心逻辑就三件事:当前页、总条数、每页几条,然后算出总页数、前后页范围、是否禁用按钮。代码也就十几行:
function SimplePagination({ total, pageSize = 10, current, onChange }) {
const totalPages = Math.ceil(total / pageSize);
const prevDisabled = current <= 1;
const nextDisabled = current >= totalPages;
return (
<div className="flex items-center gap-2">
<button
onClick={() => !prevDisabled && onChange(current - 1)}
disabled={prevDisabled}
className="px-3 py-1 rounded border disabled:opacity-50"
>
上一页
</button>
<span className="text-sm">
第 {current} 页,共 {totalPages} 页
</span>
<button
onClick={() => !nextDisabled && onChange(current + 1)}
disabled={nextDisabled}
className="px-3 py-1 rounded border disabled:opacity-50"
>
下一页
</button>
</div>
);
}
优点?一清二楚:没有 magic,状态全在眼皮底下,debug 不用翻源码;样式随便拧,Tailwind 一行搞定;也不怕哪天 Ant Design 升级把 showQuickJumper 的行为偷偷改了。缺点也很实在:没有页码跳转输入框、没有“…”省略逻辑、没有响应式断点适配(手机上一堆数字挤成一团)。我曾经在一个后台导出页里用了这个,结果运营同事非说“不能直接输页码太反人类”,最后还是换掉了——不是它不行,是业务方不认。
Ant Design 是我的“保底选项”
中大型项目,尤其团队协作、要长期维护的,我现在基本默认选 Ant Design 的 Pagination。不是因为它多牛,而是它够稳、文档够全、社区问题一搜一大把,连“如何让页码居中”这种弱智问题都有 StackOverflow 高赞答案。
import { Pagination } from 'antd';
<Pagination
current={current}
total={total}
pageSize={10}
onChange={onChange}
showQuickJumper
showSizeChanger
onShowSizeChange={onPageSizeChange}
/>
它解决了我所有“不想自己搞”的事:页码自动折叠(比如 1 2 3 … 18 19 20)、键盘支持(Tab 切换+回车确认)、无障碍属性(aria-label 全都配好了)、还有那个救命的 showQuickJumper —— 我再也不用自己写 input + blur 校验逻辑了。但这里得提个真实坑:如果你用的是 useEffect 去同步后端返回的 current,而用户快速点两下“下一页”,current 状态可能还没更新完,导致组件卡在错误页码。我踩过三次,解决办法很简单:加个防抖或强制重置 key,比如 key={current}。
那个野路子 Hook:usePagination,我私藏了半年
去年做一套数据治理平台,后端接口返回的分页字段是 page 和 page_size,但 UI 要求显示“共 127 条,第 5/13 页”,还要支持 URL 同步(?page=5&size=10)。Ant Design 没法直接塞进 URL,手搓又太碎。我就写了这么个 Hook:
function usePagination(initial = { page: 1, size: 10 }) {
const [pagination, setPagination] = useState(initial);
const update = (updates) => {
const newPage = { ...pagination, ...updates };
setPagination(newPage);
// 这里顺便 pushState,省得每个组件都处理
const url = new URL(window.location);
url.searchParams.set('page', newPage.page);
url.searchParams.set('size', newPage.size);
window.history.replaceState(null, '', url);
};
return {
...pagination,
onChange: (page) => update({ page }),
onPageSizeChange: (size) => update({ page: 1, size }),
};
}
// 使用
const { page, size, onChange, onPageSizeChange } = usePagination();
// 然后传给任意分页 UI 组件(甚至可以是上面那个手搓的)
它不绑定 UI,不强制你用某套设计语言,就干一件事:管理分页参数 + 同步 URL。我把它和一个极简的 Tailwind 分页组件配着用,代码量比 Ant Design 少一半,但关键路径(URL 同步、参数校验、默认值 fallback)全在 Hook 里兜住了。唯一缺点:没现成的“跳转到末页”按钮,得自己加,但我加了一行:onClick={() => update({ page: Math.ceil(total / size) })},完事。
性能?其实真没差多少
有人问过我“三个方案哪个渲染更快”。实测过:10w 条数据分页,切换页码时 React DevTools 里看 render 时间,差异都在 0.5ms 以内。真正卡的永远是你的 fetch 请求和列表渲染(尤其是没做虚拟滚动的时候)。别在这儿抠毫秒,先把 useCallback 包好 onChange,再检查一下有没有在分页组件里重复请求数据就行。我见过最离谱的是一次分页点击触发了 4 次相同 API,因为父组件没 memo,子组件重渲染了四遍……
我的选型逻辑
总结一下我的个人规则:
- 内部工具、MVP 快速验证 → 手搓:10 分钟搞定,不纠结
- ToB 后台、多人协作、需要长期维护 → Ant Design:省心,出问题有地方查
- 要深度定制 URL、多端统一、或者压根不用 Ant Design 主题 → usePagination Hook + 极简 UI:自由度拉满,我目前 70% 新项目走这条路
没有银弹。上周我还被迫在某个老项目里用 Vue 2 + 自己封装的 pagination mixin,因为升级 Vue 3 成本太高……所以,别迷信方案,盯紧你的场景、团队熟悉度、和下周能不能准时上线。
踩坑提醒:这三点一定注意
最后甩三个我血泪换来的提醒:
- 后端返回的 total 是 0 时,current 很可能还是 1 —— 页面会白屏或报错:一定要在 useEffect 里加 guard:
if (total === 0) setCurrent(1) - 用户手动改 URL 里的 page 参数,比如输成 ?page=abc,别直接 parseInt 就完事:我上次用
parseInt('abc')得到 NaN,然后传给后端,API 直接 500……现在一律Math.max(1, Number(page) || 1) - 移动端 touch 事件干扰分页按钮点击:某些安卓 WebView 里,快速连点两次“下一页”,第二次可能被识别为 touchmove 导致 click 失效。解决方案就一行 CSS:
button { touch-action: manipulation; }
以上是我踩坑后的总结,希望对你有帮助。这个 usePagination Hook 我稍后会开源到 GitHub(不带任何框架依赖),欢迎 star。有更优的实现方式,或者你用过其他特别顺手的分页方案,欢迎评论区交流。

暂无评论