Trying to use signals to create a shopping cart with local storage. I have two signals, the totalAmount and cart. The totalAmount is reactive, however the cart items aren’t. I’m able to retrieve the data from the signal, but if I update the quantity, or delete a menu item, the total price will change without a page refresh, but the quantity and item remain on the page.
I’ve gone through a few tutorials and gone through the docs, I feel like it should be working in my current implementation.
Here is the 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));
});
Here is the menu which displays correctly and adds to the cart correctly:
'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;
But here is the cart page which is not reactive in all places. The only place it is reactive is the "Total: ${total}" as it very simply uses the signal.
'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;
Any help is greatly appreciated. I’ve tried using useComputed() instead of useSignal() and other variations but none have allowed me to delete using the trash icon and have it reactively delete (it does delete from localstorage,but it remains on screen).
EDIT:
Here is what console.log(cartItems.value); returns:
[
{
"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
}
]
2
Answers
Can use different approach – searchParams, no need for localStorage. Created library for sharing state between unrelated client components https://github.com/asmyshlyaev177/state-in-url .
If you’re not using the Babel plugin, you haven’t followed the getting started instructions fully.
You must either use it (the Babel plugin) or the
useSignals()
hook in each of your components — this isn’t optional in React.Instructions