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

如何通过自定义Hook实现React与服务端API的正确交互?

自定义useServerCart Hook实现方案

先上可直接用的Hook代码,解决重复请求、数据格式异常问题:

import { useState, useEffect, useRef } from 'react';
import axios from 'axios';

// JS环境可跳过类型定义,TS环境保留
type CartItem = {
  id: number;
  quantity: number;
};

type CartState = {
  items: CartItem[];
  loading: boolean;
  error: string | null;
};

export function useServerCart(apiUrl: string) {
  const [cart, setCart] = useState<CartState>({
    items: [],
    loading: true,
    error: null,
  });
  // 标记首次渲染,避免初始加载后立刻触发同步请求
  const isFirstRender = useRef(true);

  // 初始加载服务端购物车数据
  useEffect(() => {
    const fetchCart = async () => {
      try {
        const response = await axios.get(apiUrl);
        // 校验并过滤无效数据,确保items格式正确
        const validItems = Array.isArray(response.data.items) 
          ? response.data.items.filter(item => typeof item.id === 'number' && typeof item.quantity === 'number')
          : [];
        setCart({
          items: validItems,
          loading: false,
          error: null,
        });
      } catch (err) {
        setCart({
          items: [],
          loading: false,
          error: '加载购物车失败',
        });
      }
    };

    fetchCart();
  }, [apiUrl]);

  // 同步购物车更新到服务端(仅非首次渲染、items变化时触发)
  useEffect(() => {
    if (isFirstRender.current) {
      isFirstRender.current = false;
      return;
    }

    const updateCart = async () => {
      try {
        // 强制格式化发送数据,严格符合服务端要求
        await axios.put(apiUrl, {
          items: cart.items.map(item => ({
            id: Number(item.id),
            quantity: Math.max(1, Number(item.quantity)) // 保证数量不小于1
          }))
        });
      } catch (err) {
        console.error('同步购物车失败:', err);
        // 可选:添加状态回滚逻辑,比如恢复上一次的有效购物车数据
      }
    };

    updateCart();
  }, [apiUrl, cart.items]);

  // 统一的购物车修改入口,确保数据格式合法
  const updateCartItem = (itemId: number, newQuantity: number) => {
    setCart(prev => {
      const existingItemIndex = prev.items.findIndex(item => item.id === itemId);
      let newItems = [...prev.items];

      if (existingItemIndex >= 0) {
        // 数量≤0则移除商品,否则更新数量
        if (newQuantity <= 0) {
          newItems = newItems.filter(item => item.id !== itemId);
        } else {
          newItems[existingItemIndex] = { ...newItems[existingItemIndex], quantity: newQuantity };
        }
      } else {
        // 新增商品仅在数量>0时生效
        if (newQuantity > 0) {
          newItems.push({ id: itemId, quantity: newQuantity });
        }
      }

      return { ...prev, items: newItems };
    });
  };

  return {
    ...cart,
    updateCartItem,
  };
}

关键解决逻辑

  • 阻断循环请求:用useRef标记首次渲染,跳过初始加载后的同步请求,避免状态更新→触发请求→状态再更新的循环。
  • 强制格式校验:不管是服务端返回还是发送给服务端的数据,都做格式校验和转换,确保items数组元素始终是{id:数字, quantity:数字}的合法结构。
  • 统一修改入口:通过updateCartItem方法修改购物车,禁止直接修改状态,从源头避免产生无效数据。
  • 错误隔离:单独处理加载和同步时的错误,避免崩溃,还可按需添加状态回滚逻辑。

使用示例

function CartComponent() {
  const { items, loading, error, updateCartItem } = useServerCart('/api/cart');

  if (loading) return <div>加载中...</div>;
  if (error) return <div>{error}</div>;

  return (
    <div>
      {items.map(item => (
        <div key={item.id}>
          商品ID: {item.id}
          <button onClick={() => updateCartItem(item.id, item.quantity - 1)}>-</button>
          <span>{item.quantity}</span>
          <button onClick={() => updateCartItem(item.id, item.quantity + 1)}>+</button>
        </div>
      ))}
    </div>
  );
}

之前方案的问题根源

  • 两个独立useEffect导致循环触发:初始加载后cartItems变化立刻触发更新请求,若服务端返回数据再次修改状态,就会陷入重复请求循环,导致数据不断追加。
  • 缺乏数据格式校验:修改状态时可能产生空数组、非数字id/quantity等无效数据,发送给服务端后出现结构异常。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 08:57:19