Search code examples
javascriptreactjsreact-hooksreact-propsreact-context

Issue trying to use useContext to update cart item with unexpected changes in another component


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?


Solution

  • Updates in Response to OP 2/18

    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.

    State Management Overview

    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?

    1. 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.

    2. 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.

    Example/Demo Full Solution with 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>
      );
    }
    

    Original Response

    It sounds like you wanted the cart to update whenever Add was clicked in MenuItem.

    Fixing use of 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.

    Example/Demo (Click to view)

    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>
      );
    }