Next.js 15X分页最佳实践咨询:客户端组件实现优化建议
Next.js 分页实现的最佳实践与优化方案
我有多年Vue开发经验,目前正在学习React.js与Next.js,开发带分页功能的用户列表页时遇到了一些问题,以下是我的代码:
"use client"; import getUsers from "@/src/lib/service/user/GetUsers"; import { Avatar, Pagination } from "@mui/material"; import { useRouter } from "next/navigation"; import { useSearchParams } from "next/navigation"; import { QueryClient, QueryClientProvider, useQuery, } from "@tanstack/react-query"; import { useState } from "react"; const queryClient = new QueryClient(); function UsersListInner() { const searchParams = useSearchParams(); const router = useRouter(); const [currentPage, setCurrentPage] = useState<number>( Number(searchParams.get("page") || 1) ); const { data } = useQuery({ queryKey: ["userList", currentPage], queryFn: () => getUsers({ page: currentPage }), staleTime: 0 }); const handleChange = (event: React.ChangeEvent<unknown>, value: number) => { router.push(`?page=${value}`); setCurrentPage(value); }; const totalPage = Math.ceil( (data?.pagination.total_count || 1) / (data?.pagination.item_per_page || 1) ); return ( <div> {data?.data.map((u) => ( <div className="border-b p-4 flex gap-2 items-center" key={u.id}> <Avatar src="/images/person.svg" /> <div>{u.fullName}</div> <div dir="ltr">{u.phone}</div> </div> ))} {data && ( <Pagination page={currentPage} count={totalPage} onChange={handleChange} /> )} </div> ); } export default function UsersList() { return ( <QueryClientProvider client={queryClient}> <UsersListInner /> </QueryClientProvider> ); }
这是一个Client Component,我使用Tanstack做缓存。请问Next.js中实现分页的最佳实践是什么?是否有更优方案?
一、当前实现的可优化点
- QueryClient 重复创建:现在每个组件单独实例化QueryClient,会导致缓存无法全局共享,应该放到全局布局中统一配置。
- 状态与URL不同步风险:先更新路由再修改state,可能出现状态和URL不一致的情况,比如路由跳转失败但state已更新。
- 缺失加载/错误状态:没有处理数据加载中、加载失败的场景,用户体验较差。
- 分页计算边界问题:data未加载时计算totalPage可能出现NaN或不合理的数值。
二、Next.js 分页最佳实践
1. 用URL参数驱动分页状态,抛弃本地state
直接从searchParams获取当前页码,不需要用useState存储,确保路由和页面状态完全同步,避免回退/前进时状态错乱:
const currentPage = Number(searchParams.get('page') || 1);
此时分页切换只需要更新路由,组件会自动从URL读取最新页码并重新请求数据。
2. 全局共享QueryClient
在app/layout.tsx中全局配置QueryClientProvider,让所有组件共享同一个缓存池,避免重复创建实例:
// app/layout.tsx 'use client'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; const queryClient = new QueryClient(); export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="zh-CN"> <body> <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> </body> </html> ); }
之后你的UsersList组件就不需要再包裹QueryClientProvider了。
3. 完善加载与错误状态处理
利用useQuery返回的isLoading、error状态,给用户明确的反馈:
const { data, isLoading, error } = useQuery({ queryKey: ['userList', currentPage], queryFn: () => getUsers({ page: currentPage }), staleTime: 0, }); if (isLoading) return <div className="p-4">加载用户列表中...</div>; if (error) return <div className="p-4 text-red-500">加载失败:{(error as Error).message}</div>;
4. 预加载相邻页面(可选优化)
如果想提升分页切换的流畅度,可以预加载下一页数据,用户点击分页时直接从缓存读取:
import { useQueryClient } from '@tanstack/react-query'; import { useEffect } from 'react'; // 在组件内添加 const queryClient = useQueryClient(); useEffect(() => { if (currentPage < totalPage) { queryClient.prefetchQuery({ queryKey: ['userList', currentPage + 1], queryFn: () => getUsers({ page: currentPage + 1 }), }); } }, [currentPage, totalPage, queryClient]);
5. 处理分页计算的边界情况
确保totalPage至少为1,避免分页组件出现异常:
const totalPage = Math.max( 1, Math.ceil((data.pagination.total_count || 0) / (data.pagination.item_per_page || 10)) );
三、优化后的完整代码示例
'use client'; import getUsers from '@/src/lib/service/user/GetUsers'; import { Avatar, Pagination } from '@mui/material'; import { useRouter, useSearchParams } from 'next/navigation'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useEffect } from 'react'; function UsersListInner() { const searchParams = useSearchParams(); const router = useRouter(); const queryClient = useQueryClient(); // 直接从URL获取当前页码 const currentPage = Number(searchParams.get('page') || 1); const { data, isLoading, error } = useQuery({ queryKey: ['userList', currentPage], queryFn: () => getUsers({ page: currentPage }), staleTime: 0, }); const handleChange = (_event: React.ChangeEvent<unknown>, value: number) => { router.push(`?page=${value}`); }; // 预加载下一页数据 useEffect(() => { if (!data) return; const totalPage = Math.max( 1, Math.ceil((data.pagination.total_count || 0) / (data.pagination.item_per_page || 10)) ); if (currentPage < totalPage) { queryClient.prefetchQuery({ queryKey: ['userList', currentPage + 1], queryFn: () => getUsers({ page: currentPage + 1 }), }); } }, [currentPage, data, queryClient]); if (isLoading) return <div className="p-4">加载用户列表中...</div>; if (error) return <div className="p-4 text-red-500">加载失败:{(error as Error).message}</div>; const totalPage = Math.max( 1, Math.ceil((data.pagination.total_count || 0) / (data.pagination.item_per_page || 10)) ); return ( <div className="space-y-4"> <div className="space-y-2"> {data.data.map((u) => ( <div className="border-b p-4 flex gap-2 items-center" key={u.id}> <Avatar src="/images/person.svg" /> <div>{u.fullName}</div> <div dir="ltr">{u.phone}</div> </div> ))} </div> <Pagination page={currentPage} count={totalPage} onChange={handleChange} className="flex justify-center" /> </div> ); } export default function UsersList() { return <UsersListInner />; }
内容的提问来源于stack exchange,提问作者morteza mortezaie
相关产品推荐
相关产品推荐

