使用Signals与LocalStorage实现购物车:组件未响应信号问题
购物车信号响应异常:总价实时更新但商品列表无刷新
我用Signals结合LocalStorage实现购物车,定义了totalAmount和cart两个信号。总价能正常响应式更新,但购物车商品条目无反应——更新数量或删除商品时,总价无需刷新页面就会变化,但页面上的商品数量和条目始终保持原样。我查阅了相关教程和文档,认为代码逻辑没问题,但就是无法实现商品列表的响应式更新。
cart.js 代码
import { signal, effect } from '@preact/signals-react'; const initialCart = JSON.parse(localStorage.getItem('cart')) || []; const initialTotalAmount = JSON.parse(localStorage.getItem('totalAmount')) || 0; export const cart = signal(initialCart); export const totalAmount = signal(initialTotalAmount); export const addToCart = (item) => { const newCart = [...cart.value]; const existingItem = newCart.find(cartItem => cartItem.id === item.id); if (existingItem) { existingItem.quantity += 1; } else { newCart.push({ ...item, quantity: 1 }); } cart.value = newCart; calculateTotal(); }; export const removeFromCart = (itemId) => { cart.value = cart.value.filter(item => item.id !== itemId); calculateTotal(); }; export const updateQuantity = (itemId, quantity) => { const newCart = [...cart.value]; const item = newCart.find(item => item.id === itemId); if (item) { item.quantity = quantity; } cart.value = newCart; calculateTotal(); }; export const clearCart = () => { cart.value = []; totalAmount.value = 0; }; export const calculateTotal = () => { totalAmount.value = cart.value.reduce((acc, item) => { const itemTotal = parseFloat(item.attributes.price) * item.quantity; return acc + itemTotal; }, 0); }; effect(() => { localStorage.setItem('cart', JSON.stringify(cart.value)); }); effect(() => { localStorage.setItem('totalAmount', JSON.stringify(totalAmount.value)); });
菜单组件(功能正常)
'use client'; import React, { useEffect, useState } from 'react'; import { Container, Typography, Card, CardContent, CardMedia, Button, Grid } from '@mui/material'; import { fetchMenuItems } from '../../lib/api'; import { addToCart } from '../../signals/cart'; const Menu = () => { const [menuItems, setMenuItems] = useState([]); useEffect(() => { const getMenuItems = async () => { try { const items = await fetchMenuItems(); setMenuItems(items.data); } catch (error) { console.error("Error fetching menu items:", error); } }; getMenuItems(); }, []); return ( <Container maxWidth="lg"> <Typography variant="h4" gutterBottom>Menu</Typography> <Grid container spacing={4}> {menuItems.map((item) => ( <Grid item key={item.id} xs={12} sm={6} md={4}> <Card> <CardMedia component="img" height="140" image={`http://localhost:1337${item.attributes.image.data.attributes.url}`} alt={item.attributes.name} /> <CardContent> <Typography variant="h5">{item.attributes.name}</Typography> <Typography variant="body2" color="textSecondary">{item.attributes.description}</Typography> <Typography variant="h6">${item.attributes.price}</Typography> <Typography variant="body2">Size: {item.attributes.size}</Typography> <div> <Button variant="contained" color="primary" onClick={() => addToCart(item)}>Add to Cart</Button> </div> </CardContent> </Card> </Grid> ))} </Grid> </Container> ); }; export default Menu;
购物车组件(响应异常)
'use client'; import React, { useEffect } from 'react'; import { Container, Typography, List, ListItem, ListItemText, IconButton, Button } from '@mui/material'; import { cart, totalAmount, removeFromCart, updateQuantity, clearCart } from '../../signals/cart'; import { useComputed } from '@preact/signals-react'; const Cart = () => { const cartItems = useComputed(() => cart.value); const total = useComputed(() => totalAmount.value); const handleIncreaseQuantity = (id) => { const item = cartItems.value.find(item => item.id === id); if (item) { updateQuantity(id, item.quantity + 1); } }; const handleDecreaseQuantity = (id) => { const item = cartItems.value.find(item => item.id === id); if (item && item.quantity > 1) { updateQuantity(id, item.quantity - 1); } }; useEffect(() => { // Debug log to verify the totalAmount and cartItems values console.log("Cart Items:", cartItems.value); console.log("Total Amount:", total); }, [cartItems, total]); return ( <Container maxWidth="sm"> <Typography variant="h4" gutterBottom>Shopping Cart</Typography> <List> {cartItems.value.length > 0 ? ( cartItems.value.map((item) => ( <ListItem key={item.id} divider> <ListItemText primary={item.attributes.name} secondary={`Price: $${item.attributes.price} x Quantity: ${item.quantity}`} /> <div> <IconButton edge="start" aria-label="decrease" onClick={() => handleDecreaseQuantity(item.id)}> ➖ </IconButton> <IconButton edge="end" aria-label="increase" onClick={() => handleIncreaseQuantity(item.id)}> ➕ </IconButton> <IconButton edge="end" aria-label="delete" onClick={() => removeFromCart(item.id)}> 🗑️ </IconButton> </div> </ListItem> )) ) : ( <Typography variant="h6">Your cart is empty.</Typography> )} </List> <Typography variant="h6">Total: ${total}</Typography> <Button variant="contained" color="primary" href="/checkout">Proceed to Checkout</Button> <Button variant="contained" color="secondary" onClick={clearCart}>Clear Cart</Button> </Container> ); }; export default Cart;
调试补充:console.log(cartItems.value)输出
[ { "id": 2, "attributes": { "name": "Hamburger", "description": "A delicious burger", "price": 5, "size": "Large", "category": "Entrees", "createdAt": "2024-07-10T20:37:57.287Z", "updatedAt": "2024-07-10T20:37:58.201Z", "publishedAt": "2024-07-10T20:37:58.200Z", "image": { "data": { "id": 1, "attributes": { "name": "pizza-5275191_1920.jpg", "alternativeText": null, "caption": null, "width": 1920, "height": 1282, "formats": { "thumbnail": { "name": "thumbnail_pizza-5275191_1920.jpg", "hash": "thumbnail_pizza_5275191_1920_00d531d4b4", "ext": ".jpg", "mime": "image/jpeg", "path": null, "width": 234, "height": 156, "size": 12.06, "sizeInBytes": 12064, "url": "/uploads/thumbnail_pizza_5275191_1920_00d531d4b4.jpg" }, "small": { "name": "small_pizza-5275191_1920.jpg", "hash": "small_pizza_5275191_1920_00d531d4b4", "ext": ".jpg", "mime": "image/jpeg", "path": null, "width": 500, "height": 334, "size": 41.47, "sizeInBytes": 41472, "url": "/uploads/small_pizza_5275191_1920_00d531d4b4.jpg" }, "large": { "name": "large_pizza-5275191_1920.jpg", "hash": "large_pizza_5275191_1920_00d531d4b4", "ext": ".jpg", "mime": "image/jpeg", "path": null, "width": 1000, "height": 668, "size": 123.53, "sizeInBytes": 123526, "url": "/uploads/large_pizza_5275191_1920_00d531d4b4.jpg" }, "medium": { "name": "medium_pizza-5275191_1920.jpg", "hash": "medium_pizza_5275191_1920_00d531d4b4", "ext": ".jpg", "mime": "image/jpeg", "path": null, "width": 750, "height": 501, "size": 79.1, "sizeInBytes": 79099, "url": "/uploads/medium_pizza_5275191_1920_00d531d4b4.jpg" } }, "hash": "pizza_5275191_1920_00d531d4b4", "ext": ".jpg", "mime": "image/jpeg", "size": 333.61, "url": "/uploads/pizza_5275191_1920_00d531d4b4.jpg", "previewUrl": null, "provider": "local", "provider_metadata": null, "createdAt": "2024-07-10T19:18:03.664Z", "updatedAt": "2024-07-10T20:37:48.434Z" } } } }, "quantity": 3 } ]
问题原因及修复方案
核心问题
你在购物车组件中直接访问信号的.value属性,破坏了Signals的响应式追踪机制:
useComputed返回的是信号对象,但你每次都通过.value获取当前值,导致React无法感知信号的后续变化。- 总价能正常更新是因为你在JSX中直接使用了
total信号对象(Signals会自动解包),而商品列表用的是cartItems.value,相当于只取了一次静态值。
修复方式(二选一)
方式1:移除useComputed,直接使用原始信号
直接在组件中引用cart.value和totalAmount.value,确保组件能追踪信号变化:
'use client'; import React from 'react'; import { Container, Typography, List, ListItem, ListItemText, IconButton, Button } from '@mui/material'; import { cart, totalAmount, removeFromCart, updateQuantity, clearCart } from '../../signals/cart'; const Cart = () => { const handleIncreaseQuantity = (id) => { const item = cart.value.find(item => item.id === id); if (item) { updateQuantity(id, item.quantity + 1); } }; const handleDecreaseQuantity = (id) => { const item = cart.value.find(item => item.id === id); if (item && item.quantity > 1) { updateQuantity(id, item.quantity - 1); } }; return ( <Container maxWidth="sm"> <Typography variant="h4" gutterBottom>Shopping Cart</Typography> <List> {cart.value.length > 0 ? ( cart.value.map((item) => ( <ListItem key={item.id} divider> <ListItemText primary={item.attributes.name} secondary={`Price: $${item.attributes.price} x Quantity: ${item.quantity}`} /> <div> <IconButton edge="start" aria-label="decrease" onClick={() => handleDecreaseQuantity(item.id)}> ➖ </IconButton> <IconButton edge="end" aria-label="increase" onClick={() => handleIncreaseQuantity(item.id)}> ➕ </IconButton> <IconButton edge="end" aria-label="delete" onClick={() => removeFromCart(item.id)}> 🗑️ </IconButton> </div> </ListItem> )) ) : ( <Typography variant="h6">Your cart is empty.</Typography> )} </List> <Typography variant="h6">Total: ${totalAmount.value}</Typography> <Button variant="contained" color="primary" href="/checkout">Proceed to Checkout</Button> <Button variant="contained" color="secondary" onClick={clearCart}>Clear Cart</Button> </Container> ); }; export default Cart;
方式2:保留useComputed,直接使用信号对象而非.value
Signals在React中会自动解包信号对象,所以在JSX中直接使用cartItems和total即可:
const Cart = () => { const cartItems = useComputed(() => cart.value); const total = useComputed(() => totalAmount.value); // ... 其他逻辑不变 return ( <Container maxWidth="sm"> <Typography variant="h4" gutterBottom>Shopping Cart</Typography> <List> {cartItems.length > 0 ? ( cartItems.map((item) => ( // ... 商品条目渲染逻辑 )) ) : ( <Typography variant="h6">Your cart is empty.</Typography> )} </List> <Typography variant="h6">Total: ${total}</Typography> // ... 其他按钮 </Container> ); };
另外,建议检查@preact/signals-react版本,确保使用最新版,旧版本可能存在自动解包的兼容性问题。
内容的提问来源于stack exchange,提问作者majordomo
相关产品推荐
相关产品推荐

