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

求助:使用Prisma、Next.js和shadcn/ui实现分页的端侧冲突问题

问题分析与分页实现方案

核心问题

你当前的代码存在两个关键问题:

  1. 客户端/服务端逻辑冲突:在客户端组件的异步函数中混用了Zustand客户端钩子与Prisma查询——Zustand钩子只能在客户端组件渲染周期内使用,而Prisma查询应当在服务端执行,直接在客户端调用会暴露数据库连接信息,同时触发Next.js的客户端钩子在服务端组件中使用的错误。
  2. 分页参数计算错误:skip和take的逻辑完全颠倒,正确的分页计算应为:skip = (当前页码 - 1) * 每页条数,take = 每页条数,你之前的写法会导致数据获取逻辑混乱。

推荐实现方案

方案一:Server Action + Zustand 客户端状态管理

适合需要在客户端维护筛选、排序等复杂状态的场景,分页状态存在内存中,切换响应快。

1. 编写服务端分页查询Action

创建app/actions/products.ts,将分页逻辑放在服务端执行:

"use server";
import prismadb from "@/lib/prismadb";
import { Products } from "@/app/store/columns";

export async function getPaginatedProducts(page: number, perPage: number) {
  // 正确计算分页参数
  const skip = (page - 1) * perPage;
  const take = perPage;

  // 并行获取数据和总条数,提升性能
  const [products, totalCount] = await Promise.all([
    prismadb.product.findMany({ skip, take }),
    prismadb.product.count(),
  ]);

  // 格式化返回数据
  const formattedProducts: Products[] = products.map((product, index) => ({
    id: skip + index + 1,
    name: product.name,
    description: product.description,
    category: product.category,
    origin: product.origin,
    sell: product.sellingPrice,
    purchase: product.purchasedPrice,
    amount: Math.random() * 100,
    status: product.status === "active" ? "active" : "inactive",
  }));

  return { products: formattedProducts, totalCount };
}

2. 修正Zustand状态配置

调整初始值为符合常规分页逻辑的设置:

import { create } from 'zustand'

interface ProductPagination {
    page: number
    perPage: number
    setPage: (page: number) => void
    setPerPage: (perPage: number) => void
}

export const useProductPagination = create<ProductPagination>()((set) => ({
    page: 1, // 初始页码设为1
    perPage: 10, // 每页默认10条
    setPage: (page: number) => set({ page }),
    setPerPage: (perPage: number) => set({ perPage })
}))

3. 重构StoreTable组件

在客户端组件中通过状态变化触发服务端查询:

"use client";
import { Products, columns } from "@/app/store/columns";
import { DataTable } from "@/app/store/data-table";
import { useProductPagination } from "@/lib/store/product-pagination";
import { getPaginatedProducts } from "@/app/actions/products";
import React, { useEffect, useState } from "react";

function StoreTable() {
  const { page, perPage, setPage } = useProductPagination();
  const [data, setData] = useState<Products[]>([]);
  const [totalCount, setTotalCount] = useState(0);

  // 分页参数变化时重新获取数据
  useEffect(() => {
    const fetchData = async () => {
      const result = await getPaginatedProducts(page, perPage);
      setData(result.products);
      setTotalCount(result.totalCount);
    };
    fetchData();
  }, [page, perPage]);

  // 修正分页按钮逻辑
  const handleNextPage = () => {
    if ((page * perPage) < totalCount) setPage(page + 1);
  };

  const handlePrevPage = () => {
    if (page > 1) setPage(page - 1);
  };

  return (
    <div className="w-screen pr-4 sm:pr-8 md:pr-10 max-w-7xl mx-auto">
      <DataTable 
        data={data} 
        columns={columns} 
        page={page}
        perPage={perPage}
        totalCount={totalCount}
        onNextPage={handleNextPage}
        onPrevPage={handlePrevPage}
      />
    </div>
  );
}

export default StoreTable;

方案二:URL参数 + 服务端组件(推荐)

符合Next.js路由优先的设计范式,支持页面刷新、分享链接,SEO友好,无需额外状态管理库。

1. 服务端页面组件(app/store/page.tsx)

直接从URL参数读取分页信息,在服务端完成数据查询:

import { Products, columns } from "./columns";
import { DataTable } from "./data-table";
import prismadb from "@/lib/prismadb";

export default async function StorePage({
  searchParams,
}: {
  searchParams: { page?: string; perPage?: string };
}) {
  const page = parseInt(searchParams.page) || 1;
  const perPage = parseInt(searchParams.perPage) || 10;
  const skip = (page - 1) * perPage;

  const [products, totalCount] = await Promise.all([
    prismadb.product.findMany({ skip, take: perPage }),
    prismadb.product.count(),
  ]);

  const formattedProducts: Products[] = products.map((product, index) => ({
    id: skip + index + 1,
    name: product.name,
    description: product.description,
    category: product.category,
    origin: product.origin,
    sell: product.sellingPrice,
    purchase: product.purchasedPrice,
    amount: Math.random() * 100,
    status: product.status === "active" ? "active" : "inactive",
  }));

  return (
    <div className="w-screen pr-4 sm:pr-8 md:pr-10 max-w-7xl mx-auto">
      <DataTable
        data={formattedProducts}
        columns={columns}
        page={page}
        perPage={perPage}
        totalCount={totalCount}
      />
    </div>
  );
}

2. 客户端分页控制组件

在DataTable中使用useRouter更新URL参数:

"use client";
import { Button } from "@/components/ui/button";
import { FiChevronLeft, FiChevronRight } from "react-icons/fi";
import { useRouter, useSearchParams } from "next/navigation";

export function PaginationControls({ page, perPage, totalCount }) {
  const router = useRouter();
  const searchParams = useSearchParams();

  const handlePageChange = (newPage: number) => {
    const params = new URLSearchParams(searchParams);
    params.set("page", newPage.toString());
    router.push(`?${params}`);
  };

  const canNextPage = (page * perPage) < totalCount;
  const canPrevPage = page > 1;

  return (
    <div className="flex items-center justify-between mt-4">
      <Button
        variant="outline"
        className="h-8 w-8 p-0"
        onClick={() => handlePageChange(page - 1)}
        disabled={!canPrevPage}
      >
        <span className="sr-only">上一页</span>
        <FiChevronLeft className="h-4 w-4" />
      </Button>
      <Button
        variant="outline"
        className="h-8 w-8 p-0"
        onClick={() => handlePageChange(page + 1)}
        disabled={!canNextPage}
      >
        <span className="sr-only">下一页</span>
        <FiChevronRight className="h-4 w-4" />
      </Button>
    </div>
  );
}

内容的提问来源于stack exchange,提问作者Mohammed Bekele

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 03:42:33