Next.js 13购物车开发:useState无限循环与Hook调用错误排查
问题原因分析
- 服务器组件与Hooks冲突:Next.js App Router中,
async组件默认是服务器组件,服务器组件不允许使用React Hooks(如useState),这会触发Invalid hook call错误;同时Hooks必须在组件顶层调用,放在fetch之后违反了Hook规则,导致params解构错误。 - 无限渲染循环:直接在组件渲染流程中执行
await client.fetch,每次状态更新(点击+/按钮、切换图片)都会重新渲染组件,进而重复执行数据请求,形成无限循环。
解决方案:拆分组件,分离数据获取与交互逻辑
将组件拆分为两部分:服务器组件负责数据获取,客户端组件处理交互状态。
1. 服务器组件(page.tsx)
仅负责从Sanity获取数据,传递给客户端组件,不会处理任何交互逻辑。
// app/[product]/[productitems]/page.tsx import client from '@/sanity/client'; import ProductItemClient from './ProductItemClient'; export default async function ProductItemPage({ params }: { params: { productitem: string, product: string } }) { // 服务器端执行数据请求,仅初始化时执行一次 const response = await client.fetch(`*[_type=="product" && _id == "${params.productitem}" && category->name == "${params.product}"]{ _id,dressType,image,category->{name}, }`); // 处理数据为空的情况 if (!response || response.length === 0) { return <div>产品不存在</div>; } return <ProductItemClient product={response[0]} />; }
2. 客户端组件(ProductItemClient.tsx)
标记为客户端组件('use client'),使用useState处理交互状态,接收服务器传递的产品数据。
// app/[product]/[productitems]/ProductItemClient.tsx 'use client'; // 必须标记为客户端组件才能使用Hooks import { useState } from 'react'; import Image from 'next/image'; import { Button } from '@/components/ui/button'; import { ShoppingCart } from 'lucide-react'; import urlForImage from '@/sanity/imageUrl'; // 定义产品类型,避免any类型 type Product = { _id: string; dressType: string; image: Array<{ _key: string; asset: { _ref: string } }>; category: { name: string }; }; export default function ProductItemClient({ product }: { product: Product }) { const [quantity, setQuantity] = useState(1); // 初始化直接使用第一张图片地址 const [imageURL, setImageUrl] = useState(urlForImage(product.image[0]).url()); // 修复数量增减逻辑 const handleDecrease = () => { if (quantity > 1) { setQuantity(prev => prev - 1); } }; const handleIncrease = () => { setQuantity(prev => prev + 1); }; return <> <div className='mt-8 mb-12 flex flex-col lg:flex-row lg:gap-4 justify-center gap-2 mx-4 border-b-4'> <div className='flex flex-row lg:flex-col gap-2 justify-center rounded m-4 px-4'> {product.image.map(item => { const imgUrl = urlForImage(item).url(); return <Image src={imgUrl} alt="产品缩略图" width={100} height={125} className='max-h-[125px] max-w-[100px] object-contain object-top cursor-pointer' key={item._key} onClick={() => setImageUrl(imgUrl)} />; })} </div> <div className='rounded flex justify-center my-4 mx-2'> <Image src={imageURL} alt="主产品图" width={315} height={450} className='max-h-[450px] h-auto max-w-full object-contain object-top' /> </div> <div className='lg:my-4 lg:mx-2 flex flex-col lg:p-4 mx-auto'> <h1 className='text-xl font-semibold'>BRUSHED RAGLAN SWEATSHIRT</h1> <h4 className='text-lg font-bold text-gray-500'>{product.category.name}</h4> <div className='mt-5 flex flex-col'> <h5 className='text-lg font-semibold'>选择尺码</h5> <div className='text-lg my-1 space-x-8 py-1'> <span className='rounded border-2 inline font-bold p-2'>XL</span> <span className='rounded border-2 inline font-bold p-2'>L</span> <span className='rounded border-2 inline font-bold p-2'>M</span> <span className='rounded border-2 inline font-bold p-2'>S</span> </div> <div className='my-4 space-y-2'> <label className='font-bold'>数量:</label> <div className='flex flex-row gap-x-2 items-center'> <Button className='border-2' onClick={handleDecrease}> - </Button> <h6 className='text-xl font-bold'>{quantity}</h6> <Button className='border-2' onClick={handleIncrease}> + </Button> <h4 className='mx-10 text-2xl font-bold'>$ 195.00</h4> </div> <Button className='my-2'> <ShoppingCart className="mr-2 h-4 w-4" /> 加入购物车 </Button> </div> </div> </div> </div> <div className='mt-8 flex flex-col flex-wrap sm:max-w-[65%] gap-2 justify-center mx-auto mb-8'> <h1 className='text-2xl font-bold px-2'>产品信息</h1> <p className='text-justify text-lg text-gray-500 px-2'>Lorem ipsum dolor sit amet consectetur, adipisicing elit. Expedita earum aspernatur sapiente ut amet quae odit eius totam! Et cupiditate dolores eligendi. Maxime veniam nemo quia eveniet harum mollitia possimus?</p> </div> </>; }
额外修复点
- 修正了减号按钮的逻辑错误(原代码中减号按钮执行的是
quantity + 1) - 初始化
imageURL时直接使用第一张图片地址,简化条件判断 - 提取图片URL计算逻辑,避免重复调用
urlForImage - 给缩略图添加
cursor-pointer样式,提升交互体验
内容的提问来源于stack exchange,提问作者Faizan Tariq
相关产品推荐
相关产品推荐

