I am studying Front end development and am writing simple shopping cart in React to learn. When I change product quantity I want to create an object of quantity values by product id and pass them to shopping cart component to display later. The quantities data that I gather seems to be correct, because I can see them displaying correctly in QTY: state as shown in this picture:
,but when I pass quantities state to the parent and then from a parent to a shopping cart component and console.log it - the data seems to be all over the place... Sometimes it is not picked up at all, sometimes too small by 1 and so on...
I've seen some videos where people build shopping cart and they seem to use state management tools like react useContext or Redux and I get it global state would be much nicer than passing data from child to parent, then from parent to another child, but I think I am not yet ready for state management tools.
App component:
import { useState } from "react";
import ProductCard from "./components/ProductCard";
import ShoppingCart from "./components/ShoppingCart";
function App() {
let [productData, setProductData] = useState({});
function getProductData(cartData: object) {
setProductData(cartData);
}
return (
<>
<div>
<ProductCard passProductData={getProductData} />
<ShoppingCart productData={productData} />
</div>
</>
);
}
export default App;
Cart Component:
interface Props {
productData: object;
}
function ShoppingCart({ productData }: Props) {
return (
<>
<button
onClick={() => {
console.log(JSON.stringify(productData));
}}
className="bg-slate-400"
>
Test passed data
</button>
<h1 className="font-bold">Your cart</h1>
<p className="font-bold">Order Total</p>
</>
);
}
export default ShoppingCart;
Product card component:
import productList from "../data/productList";
import { useState } from "react";
interface Props {
passProductData: (quantities: object) => void;
}
function ProductCard({ passProductData }: Props) {
let [newProductList, changeProductList] = useState(productList);
let [productQuantities, setProductQuantities] = useState({
0: 0,
1: 0,
2: 0,
3: 0,
4: 0,
5: 0,
});
function revealQuantityInput(id: number) {
changeProductList(
newProductList.map((product) => {
if (product.id === id) {
return { ...product, quantityVisibility: true };
} else return product;
})
);
}
function increaseQuantity(id: number) {
for (let [key, value] of Object.entries(productQuantities)) {
if (Number(key) === id) {
setProductQuantities({
...productQuantities,
[key]: value + 1,
});
}
}
passProductData(productQuantities);
}
function decreaseQuantity(id: number) {
for (let [key, value] of Object.entries(productQuantities)) {
if (Number(key) === id) {
setProductQuantities({
...productQuantities,
[key]: value > 0 ? value - 1 : value,
});
}
}
passProductData(productQuantities);
}
function setSpecificQuantityValue(id: number, quantity: number) {
newProductList.map((product) => {
if (product.id === id) {
return setProductQuantities({
...productQuantities,
[product.id]: quantity,
});
}
});
passProductData(productQuantities);
}
return (
<>
{newProductList.map((product) => (
<div className="p-6 mb-6 [&>*]:mb-4" key={product.id}>
<img src={product.image} />
<div
className="bg-green-600 p-2"
onClick={() => revealQuantityInput(product.id)}
>
{product.quantityVisibility ? (
<div>
<p onClick={() => decreaseQuantity(product.id)}>-</p>
<input
type="number"
id={"quantity" + product.id}
min="1"
max="9"
step="1"
onChange={(e) =>
setSpecificQuantityValue(product.id, Number(e.target.value))
}
className="bg-gray-300 w-[150px] opacity-100 mr-3"
/>
<p onClick={() => increaseQuantity(product.id)}>+</p>
</div>
) : (
<p>Add to Cart</p>
)}
</div>
<p>{product.category}</p>
<p>{product.name}</p>
<p>{product.price}</p>
<p>QTY: {JSON.stringify(productQuantities)}</p>
</div>
))}
</>
);
}
export default ProductCard;
Product data:
import productTypes from "../types/productTypes"
const productList:productTypes[] = [
{
id: 0,
image: "../../images/image-waffle-mobile.jpg",
name: "Waffle with Berries",
category: "Waffle",
price: 6.50,
quantityVisibility: false,
},
{
id: 1,
image: "../../images/image-creme-brulee-mobile.jpg",
name: "Vanilla Bean Crème Brûlée",
category: "Crème Brûlée",
price: 7.00,
quantityVisibility: false,
},
{
id: 2,
image: "../../images/image-macaron-mobile.jpg",
name: "Macaron Mix of Five",
category: "Macaron",
price: 8.00,
quantityVisibility: false,
},
{
id: 3,
image: "../../images/image-tiramisu-mobile.jpg",
name: "Classic Tiramisu",
category: "Tiramisu",
price: 5.50,
quantityVisibility: false,
},
{
id: 4,
image: "../../images/image-baklava-mobile.jpg",
name: "Pistachio Baklava",
category: "Baklava",
price: 4.00,
quantityVisibility: false,
},
{
id: 5,
image: "../../images/image-meringue-mobile.jpg",
name: "Pistachio Baklava",
category: "Baklava",
price: 4.00,
quantityVisibility: false,
},
]
export default productList
Thank you in advance!
Regards, Darius
You have a few anti-patterns and various issues in your code.
productData
state from App
is effectively duplicated in Productcard
and there exists state synchronicity issues. This is why you see the mis-matched states in the console log.ProductCard
is enqueueing state updates to the parent component immediately after enqueueing a productQuantities
state update. React state updates are not immediately processed, so the old state is passed to the parent. This further leads to quantity discrepancies in the console logs.The productData
state in App
should be the single source of truth.
Update App
to pass down both productData
and setProductData
to the ProductCard
component. Use a lazy state initializer function to initialize the state with the initial 0 quantities by product id.
import { useState } from "react";
import ProductCard from "./components/ProductCard";
import ShoppingCart from "./components/ShoppingCart";
import productList from "./data/productList";
export default function App() {
const [productData, setProductData] = useState<Record<string, number>>(() =>
productList.reduce(
(quantities, { id }) => ({
...quantities,
[id]: 0,
}),
{}
)
);
return (
<div>
<ProductCard setProductData={setProductData} productData={productData} />
<ShoppingCart productData={productData} />
</div>
);
Update ProductCard
component to enqueue state updates directly to the parent state. Use functional state updates to correctly update from the previous state value.
import productListData from "../data/productList";
import { useState } from "react";
interface Props {
setProductData: React.Dispatch<React.SetStateAction<Record<string, number>>>;
productData: Record<string, number>;
}
function ProductCard({ setProductData, productData }: Props) {
const [productList, changeProductList] = useState(productListData);
function revealQuantityInput(id: number) {
changeProductList((productList) =>
productList.map((product) =>
product.id === id
? { ...product, quantityVisibility: true }
: product
)
);
}
function increaseQuantity(id: number) {
setProductData((quantities) => ({
...quantities,
[id]: quantities[id] + 1,
}));
}
function decreaseQuantity(id: number) {
setProductData((quantities) => ({
...quantities,
[id]: quantities[id] - 1,
}));
}
function setSpecificQuantityValue(id: number, quantity: number) {
setProductData((quantities) => ({
...quantities,
[id]: quantity,
}));
}
return productList.map((product) => (
<div className="p-6 mb-6 [&>*]:mb-4" key={product.id}>
<img src={product.image} />
<div
className="bg-green-600 p-2"
onClick={() => revealQuantityInput(product.id)}
>
{product.quantityVisibility ? (
<div>
<p onClick={() => decreaseQuantity(product.id)}>-</p>
<input
type="number"
id={"quantity" + product.id}
min="1"
max="9"
step="1"
onChange={(e) =>
setSpecificQuantityValue(product.id, Number(e.target.value))
}
className="bg-gray-300 w-[150px] opacity-100 mr-3"
/>
<p onClick={() => increaseQuantity(product.id)}>+</p>
</div>
) : (
<p>Add to Cart</p>
)}
</div>
<p>{product.category}</p>
<p>{product.name}</p>
<p>{product.price}</p>
<p>QTY: {JSON.stringify(productData)}</p>
</div>
));
}
export default ProductCard;