I just started learning React and came across this problem where I have two components - one for showing menu items called MenuItems
and one called Cart
for the cart.
The idea is that menu items can be added to the cart. I also want to have the ability for users to add or remove an item from inside the Cart
component, but currently I'm unsure how to get this working.
I'm providing handler functions passed as props to my menu item for the functionality of adding a selected quantity of a specific item to the cart. This adds the item to the cart including item details and quantity.
Now, I want a similar functionality inside the cart. I know there should be some way without repeating the entire logic again. I know this is a long one. Thanks in advance for answering!!!
App.js
import react, { useState, useEffect } from "react";
import Header from "./components/Header";
import LandingPage from "./components/LandingPage";
import MenuItems from "./components/menuItems";
import Cart from "./components/Cart";
import ItemContext from "./store/item-context";
function App() {
const [items, setItems] = useState([]);
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(() => {
return items.reduce((acc, eachItem) => {
return eachItem.quantity + acc;
}, 0)
})
}, [items])
const [cartBool, setCartBool] = useState(false);
function AddedItem(item) {
const foundIndex = items.findIndex(eachItem => {
return eachItem.title === item.title;
})
if (foundIndex !== -1) {
setItems(prev => {
prev[foundIndex].quantity = item.quantity;
return [...prev];
})
}
else {
setItems(prev => {
return [...prev, item]
})
}
}
function handleCartClick() {
setCartBool(true);
}
function handleCloseClick() {
setCartBool(false);
}
return (
<react.Fragment>
<ItemContext.Provider value={{
items: items
}}>
{cartBool &&
<Cart onCloseClick={handleCloseClick} />}
<div className="parent-container">
<Header cartCount={total} onCartClick={handleCartClick} />
<LandingPage />
<MenuItems onAddItem={AddedItem} />
</div>
</ItemContext.Provider>
</react.Fragment>
);
}
export default App;
Menu-items.js
import react from "react";
import MenuItem from "./menuItem";
import MenuContent from "./menuContent";
function MenuItems(props) {
function handleItems(item){
props.onAddItem(item);
}
return (
<div className="menu">
{MenuContent.map(eachItem =>{
return <MenuItem title={eachItem.title} description={eachItem.description} price={eachItem.price} key={eachItem.key} onAdd={handleItems}/>
})}
</div>
);
}
export default MenuItems;
Menu-item.js
import react , { useState } from "react";
function MenuItem(props) {
const [item, setItem] = useState({
title: "",
quantity: 0,
price: ""
});
function handleClick(){
setItem(prev =>{
return {
title: props.title,
quantity: prev.quantity + 1,
price: props.price
}
})
}
function handleSubmit(event){
event.preventDefault();
props.onAdd(item);
}
return (
<div className="menu-item">
<div className="menu-content">
<h3>{props.title}</h3>
<p>{props.description}</p>
<h4>{props.price}</h4>
</div>
<form onSubmit={handleSubmit} className="add-items">
<label htmlFor="Amount">Amount</label>
<input onChange={() => {}} type="number" name="Amount" value={item.quantity}/>
<button onClick={handleClick} type="submit" className="btn btn-lg">Add</button>
</form>
</div>
);
}
export default MenuItem;`
Cart.js
import react, { useContext } from "react";
import CartItem from "./cartItem";
import ItemContext from "../store/item-context";
function Cart(props) {
const ctx = useContext(ItemContext);
function handleCloseClick(){
props.onCloseClick();
}
return (
<div className="cart-modal">
<div className="card">
{ctx.items.map((eachItem, index) =>{
return <CartItem title={eachItem.title} price={eachItem.price} quantity={eachItem.quantity} key={index} onAdd={props.onAddItem} onRemove={props.RemoveItem}/>
})}
<footer>
<button className="btn btn-lg" onClick={handleCloseClick}>Close</button>
<button className="btn btn-lg">Order</button>
</footer>
</div>
</div>
);
}export default Cart;
cartItem.js
import react, { useState } from "react";
function CartItem(props) {
const [item, setItem] = useState({
title: props.title,
price: props.price,
quantity: props.quantity
})
function handlePlusClick(){
setItem(prev =>{
prev.quantity = prev.quantity + 1
return prev
})
props.onAdd(item);
}
function handleMinusClick(){
var updatedQuantity;
setItem(prev =>{
prev.quantity = prev.quantity -1
updatedQuantity = prev.quantity
return prev;
})
if(updatedQuantity > 0){
props.onAdd(item);
}
else{
props.onRemove(item);
}
}
return (
<div className="cart-item">
<div className="cart-content">
<h1>{props.title}</h1>
<p>{props.price}
<span> X {props.quantity}</span>
</p>
</div>
<div className="button-controls">
<button onClick={handleMinusClick}>-</button>
<button onClick={handlePlusClick}>+</button>
</div>
</div>
);
}export default CartItem;
I tried creating a new item object when user clicked on the +
button in CartItem
and sent it to AddedItem
function in App
. It works, however, it is also updating the item.quantity
for the item inside of my MenuItem
component too. I am not sure why it is going back and updating the MenuItem
quantity as well. Is it because of the useContext
I wrapped around all the components I'm rendering?
Your example is still a bit hard to follow and reproduce since we can't see MenuContent
and the use of useContext
is confusing.
But it sounds like both your menu and the cart are using the same items
state or at least something along those lines is happening.
Your code demonstrates a handle on state management but I think you need to take a step back and think about what parts of your app should be stateful and what strategies are needed. You don't need useContext
but I suppose it's an opportunity to illustrate the differences and advantages.
For now I'll assume your menu items are a list of items that aren't really changing. You cart will need some state since you need to track the items along with their quantity and use this information to calculate cart totals.
Where do we need to update or access our cart state?
MenuItem
- Our menu item has an Add
button that should update the cart state with the new quantity. We don't need the cart items here, but we do need to handle the logic to update our cart.
Cart
- Our cart needs to access the cart state to a) show the cart items and b) to increment or decrement the quantity of specific items (+ and -).
You can do this with prop drilling using the same strategies used in your code so far (that you've shared) OR you can use useContext
.
To demonstrate the difference, below is a more complete solution with useContext
. All state management logic for the cart is bundled into our cart context and our provider lets parts of our app access this without relying so much on props.
useContext
(Click to View)https://codesandbox.io/s/update-cart-example-use-context-4glul7
import "./styles.css";
import React, { useState, createContext, useContext, useReducer } from "react";
const CartContext = createContext();
const initialCartState = { cartItems: [], totalCost: 0, totalQuantity: 0 };
const actions = {
INCREMENT_ITEM: "INCREMENT_ITEM",
DECREMENT_ITEM: "DECREMENT_ITEM",
UPDATE_QUANTITY: "UPDATE_QUANTITY"
};
const reducer = (state, action) => {
const existingCartItem = state.cartItems.findIndex((item) => {
return item.id === action.itemToUpdate.id;
});
switch (action.type) {
case actions.INCREMENT_ITEM:
return {
cartItems: state.cartItems.map((item) =>
item.id === action.itemToUpdate.id
? {
...item,
quantity: item.quantity + 1
}
: item
),
totalQuantity: state.totalQuantity + 1,
totalCost: state.totalCost + action.itemToUpdate.price
};
case actions.DECREMENT_ITEM:
return {
cartItems: state.cartItems.map((item) =>
item.id === action.itemToUpdate.id
? {
...item,
quantity: item.quantity - 1
}
: item
),
totalQuantity: state.totalQuantity - 1,
totalCost: state.totalCost - action.itemToUpdate.price
};
case actions.UPDATE_QUANTITY:
return {
cartItems:
existingCartItem !== -1
? state.cartItems.map((item) =>
item.id === action.itemToUpdate.id
? {
...item,
quantity: item.quantity + action.itemToUpdate.quantity
}
: item
)
: [...state.cartItems, action.itemToUpdate],
totalQuantity: state.totalQuantity + action.itemToUpdate.quantity,
totalCost:
state.totalCost +
action.itemToUpdate.quantity * action.itemToUpdate.price
};
default:
return state;
}
};
const CartProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialCartState);
const value = {
cartItems: state.cartItems,
totalQuantity: state.totalQuantity,
totalCost: state.totalCost,
incrementItem: (itemToUpdate) => {
dispatch({ type: actions.INCREMENT_ITEM, itemToUpdate });
},
decrementItem: (itemToUpdate) => {
dispatch({ type: actions.DECREMENT_ITEM, itemToUpdate });
},
updateQuantity: (itemToUpdate) => {
dispatch({ type: actions.UPDATE_QUANTITY, itemToUpdate });
}
};
return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
};
export default function App() {
return (
<CartProvider>
<MenuItems />
<Cart />
</CartProvider>
);
}
const menuItems = [
{ title: "item 1", description: "description 1", price: 10, id: "1" },
{ title: "item 2", description: "description 2", price: 20, id: "2" },
{ title: "item 3", description: "description 3", price: 30, id: "3" }
];
function MenuItems(props) {
return (
<div className="menu">
{menuItems.map((item) => {
return (
<MenuItem
title={item.title}
description={item.description}
price={item.price}
key={item.id}
// added this as prop
id={item.id}
/>
);
})}
</div>
);
}
function MenuItem(props) {
const { updateQuantity } = useContext(CartContext);
const [item, setItem] = useState({
title: props.title,
quantity: 0,
price: props.price,
// included a unique item id here
id: props.id
});
// Don't need this anymore...
// function handleClick(e) {
// ...
// }
// update quantity as we type by getting as state...
function changeQuantity(e) {
e.preventDefault();
setItem((prev) => {
return {
...prev,
quantity: Number(e.target.value)
};
});
}
function handleSubmit(e, item) {
e.preventDefault();
updateQuantity(item);
}
return (
<div className="menu-item">
<div className="menu-content">
<h3>{props.title}</h3>
<p>{props.description}</p>
<h4>Price: ${props.price}</h4>
</div>
<form onSubmit={(e) => handleSubmit(e, item)} className="add-items">
<label htmlFor="Amount">Amount</label>
<input
onChange={changeQuantity}
type="number"
name="Amount"
value={item.quantity}
/>
{/* No need for onClick on button, onSubmit already handles it */}
<button type="submit" className="btn btn-lg">
Add
</button>
</form>
</div>
);
}
function Cart() {
const {
cartItems,
totalQuantity,
totalCost,
incrementItem,
decrementItem
} = useContext(CartContext);
return (
<div>
<h2>Cart</h2>
<h3>Items:</h3>
{cartItems.length > 0 &&
cartItems.map(
(item) =>
item.quantity > 0 && (
<div key={item.id}>
{item.title}
<br />
<button onClick={() => decrementItem(item)}> - </button>{" "}
{item.quantity}{" "}
<button onClick={() => incrementItem(item)}> + </button>
</div>
)
)}
<h3>Total Items: {totalQuantity}</h3>
<h3>Total Cost: {`$${Number(totalCost).toFixed(2)}`}</h3>
</div>
);
}
It sounds like you wanted the cart to update whenever Add
was clicked in MenuItem
.
onClick
and onSubmit
This was part of your issue. In MenuItem
you used a form and had onClick
on your form submit button. Since your button has type="submit"
it will fire submit event along with onSubmit
handler. We can simply use onSubmit
as our handler here and remove the onClick
from the button.
I simplified MenuItem
to update and read quantity value from state. Then when adding the item we simply pass the item (since it already has the up-to-date quantity).
Your logic was basically there. I gave each product an id
to simplify keeping track with all the prop drilling versus using title
or key
as it was just a bit easier for me to wrap my head around. Hopefully the changes and comments make sense.
https://codesandbox.io/s/update-cart-example-veic1h
import "./styles.css";
import React, { useState, createContext, useContext, useEffect } from "react";
const CartContext = createContext();
export default function App() {
const [cartItems, setCartItems] = useState([]);
const [totalQuantity, setTotalQuantity] = useState(0);
const [totalCost, setTotalCost] = useState(0);
useEffect(() => {
setTotalQuantity(() => {
return cartItems.reduce((acc, item) => {
return item.quantity + acc;
}, 0);
});
setTotalCost(() => {
return cartItems.reduce((acc, item) => {
return item.quantity * item.price + acc;
}, 0);
});
}, [cartItems]);
function addItemToCart(newItem) {
const existingCartItem = cartItems.findIndex((item) => {
return item.id === newItem.id;
});
setCartItems((prevItems) => {
return existingCartItem !== -1
? prevItems.map((prevItem) =>
prevItem.id === newItem.id
? {
...prevItem,
quantity: prevItem.quantity + newItem.quantity
}
: prevItem
)
: [...prevItems, newItem];
});
// the above is similar to what you have below,
// but good practice not to mutate state directly
// in case of incrementing item already found in cart...
// if (foundIndex !== -1) {
// setCartItems((prev) => {
// prev[foundIndex].quantity = item.quantity;
// return [...prev];
// });
// } else {
// setCartItems((prev) => {
// return [...prev, item];
// });
// }
}
return (
<CartContext.Provider value={{ cartItems, totalQuantity, totalCost }}>
<div className="parent-container">
<MenuItems onAddItem={addItemToCart} />
<Cart />
</div>
</CartContext.Provider>
);
}
const menuItems = [
{ title: "item 1", description: "description 1", price: 10, id: "1" },
{ title: "item 2", description: "description 2", price: 20, id: "2" },
{ title: "item 3", description: "description 3", price: 30, id: "3" }
];
function MenuItems(props) {
function handleItems(item) {
props.onAddItem(item);
}
return (
<div className="menu">
{menuItems.map((item) => {
return (
<MenuItem
title={item.title}
description={item.description}
price={item.price}
key={item.id}
// added this as prop
id={item.id}
onAdd={handleItems}
/>
);
})}
</div>
);
}
function MenuItem(props) {
const [item, setItem] = useState({
title: props.title,
quantity: 0,
price: props.price,
// included a unique item id here
id: props.id
});
// Don't need this anymore...
// function handleClick(e) {
// ...
// }
// update quantity as we type by getting as state...
function changeQuantity(e) {
e.preventDefault();
setItem((prev) => {
return {
...prev,
quantity: Number(e.target.value)
};
});
}
function handleSubmit(event) {
event.preventDefault();
props.onAdd(item);
}
return (
<div className="menu-item">
<div className="menu-content">
<h3>{props.title}</h3>
<p>{props.description}</p>
<h4>Price: ${props.price}</h4>
</div>
<form onSubmit={handleSubmit} className="add-items">
<label htmlFor="Amount">Amount</label>
<input
onChange={changeQuantity}
type="number"
name="Amount"
value={item.quantity}
/>
{/* No need for onClick on button, onSubmit already handles it */}
<button type="submit" className="btn btn-lg">
Add
</button>
</form>
</div>
);
}
function Cart() {
const cart = useContext(CartContext);
const { cartItems, totalQuantity, totalCost } = cart;
return (
<div>
<h2>Cart</h2>
<h3>Items:</h3>
{cartItems.length > 0 &&
cartItems.map(
(item) =>
item.quantity > 0 && (
<div key={item.id}>
{item.title} - quantity: {item.quantity}
</div>
)
)}
<h3>Total Items: {totalQuantity}</h3>
<h3>Total Cost: {`$${Number(totalCost).toFixed(2)}`}</h3>
</div>
);
}