如何通过自定义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
相关产品推荐
相关产品推荐

