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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 22:26:00