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

Next.js 13购物车开发:useState无限循环与Hook调用错误排查

问题原因分析
  1. 服务器组件与Hooks冲突:Next.js App Router中,async组件默认是服务器组件,服务器组件不允许使用React Hooks(如useState),这会触发Invalid hook call错误;同时Hooks必须在组件顶层调用,放在fetch之后违反了Hook规则,导致params解构错误。
  2. 无限渲染循环:直接在组件渲染流程中执行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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 16:13:26