求助:使用Prisma、Next.js和shadcn/ui实现分页的端侧冲突问题
问题分析与分页实现方案
核心问题
你当前的代码存在两个关键问题:
- 客户端/服务端逻辑冲突:在客户端组件的异步函数中混用了Zustand客户端钩子与Prisma查询——Zustand钩子只能在客户端组件渲染周期内使用,而Prisma查询应当在服务端执行,直接在客户端调用会暴露数据库连接信息,同时触发Next.js的客户端钩子在服务端组件中使用的错误。
- 分页参数计算错误:
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
相关产品推荐
相关产品推荐

