Search code examples
javascriptreactjsfrontendjsxreact-state

React state showing weird behavior


I am new to react and making a simple shopping cart for a coffee shop. whenever i add a item to cart if the item is not already in cart then i basically add it in with the rest of the items and set quantity property to 1 and whenever i add the same item again i just take the whole items and update the corresponding items quantity by doing quantity++. But i don't know why the quantity gets updated by two on its own. i have tried debugging it but i cant get it that why is this happening that why isn't it updating by 1 and instead getting updated by 2.

Here is my app component -:

import CoffeeShop from "./CoffeeShop"

function App() {

  return (
    <>
      <h1 style={{ textAlign: "center" }}>Coffee Shop</h1>
      <CoffeeShop />
    </>
  )
}

export default App

This is my CoffeeShop component -:

In this component i am handling the add to cart behavior

import "./CoffeeShop.css";
import CoffeeItems from "./CoffeeItems";
import ShoppingCart from "./ShoppingCart";
import { useState } from "react";

export default function CoffeeShop() {

    const [cartItems, setCartItems] = useState([]);
    const [totalPrice, setTotalPrice] = useState(0);

    const addToCart = (id, item) => {
        console.log(cartItems);
        setCartItems(currItems => {
            const idx = currItems.findIndex(cartItem => cartItem.id === id);
            if (idx !== -1) {
                const newItems = [...currItems];
                newItems[idx].quantity++;
                return newItems;
            }
            return [...currItems, { ...item, quantity: 1 }];
        });
        setTotalPrice(currTotalPrice => currTotalPrice + item.price);
    };



    return (
        <div className="CoffeeShop">
            <CoffeeItems addToCart={addToCart} />
            <ShoppingCart cartItems={cartItems} totalPrice={totalPrice} />
        </div>
    );
}

This is my CoffeeItems component -:

import { useEffect } from "react";
import { useState } from "react";
import fetchProducts from "./productsApi";
import CoffeeItem from "./CoffeeItem";
import "./CoffeeItems.css";
import { v4 as uid } from "uuid";

export default function CoffeeItems({ addToCart }) {

    const [products, setProducts] = useState([]);
    const [isLoading, setIsLoading] = useState(true);

    useEffect(() => {
        const fetchData = async () => {
            setIsLoading(true);
            const data = await fetchProducts();
            setIsLoading(false);
            for (let product of data) {
                product.id = uid();
            }
            setProducts(data);
        }
        fetchData();

    }, []);


    return (
        <div className="CoffeeItems">

            {isLoading && <h1>Loading...</h1>}
            {products.map(product => <CoffeeItem product={product} addToCart={addToCart} key={product.id} />)}

        </div>
    );
}

**The CoffeeItems component basically fetches mock data where each product like : **

{
        "id": 3,
        "name": "Tea set",
        "price": 39.99,
        "description": "This tea set is a complete tea-making solution for tea enthusiasts. It consists of a beautiful and functional kettle for boiling water and a set of delicate tea cups designed for serving tea. The kettle is made of ceramic and comes with a handle and spout for easy pouring. The tea cups are small and dainty with a simple yet elegant design. Whether for a quiet afternoon tea with a friend, a family gathering, or a special event, this tea set offers a classic and sophisticated way to enjoy a relaxing cup of tea.",
        "image": "alisher-sharip-mumpl9-D7Uc-unsplash.jpg"
    }

This is my CoffeeItem component -:

import "./CoffeeItem.css";

export default function CoffeeItem({ product, addToCart }) {

    const handleAddToCart = (e) => {
        e.preventDefault();
        addToCart(product.id, product);
    }

    return (
        <div className="CoffeeItem">
            <h2>{product.name}- ${product.price}</h2>
            <img src={product.image} alt="product image" />
            <div>
                <button onClick={handleAddToCart}>Add to cart</button>
            </div>
        </div>
    );
}

This is my ShoppingCart component -:

import "./ShoppingCart.css";
import CartItem from "./CartItem";

export default function ShoppingCart({ cartItems, totalPrice }) {

    return (
        <div className="ShoppingCart">
            <h1>Shopping Cart</h1>
            {cartItems.length === 0 && <h2>Your cart is empty</h2>}
            {cartItems.length > 0 && <h2>total: ${totalPrice}</h2>}
            {cartItems.map(cartItem => <CartItem cartItem={cartItem} key={cartItem.id} />)}
        </div>
    );
}

**This is my CartItem component -: **

import "./CartItem.css";

export default function CartItem({ cartItem }) {
    return (
        <div className="CartItem">
            <h2>Name: {cartItem.name}</h2>
            <h3>Price: ${cartItem.price}</h3>
            <h3>Quantity: {cartItem.quantity}</h3>
        </div>
    );
}

In the CoffeeShop component when addCart handler is executed for a some item which is already in the cart it in the code it adds +1 right. but it actually adds +2 to the quantity property of the respective product. I don't know why is this happening. Please help.

EDIT

I just removed from the main.jsx file and everything is working fine now. but why ?

This is the main.jsx file

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
import './index.css'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

**When i remove everything works fine. but why ? **


Solution

  • It's subtle, but you're mutating state:

    newItems[idx].quantity++;
    

    This is always to be avoided in React and can cause bugs like what you're seeing. (Specifically what you're seeing is also the result of StrictMode rendering components twice.)

    Fundamentally, never mutate state in React. Making a shallow copy of the array is not enough:

    const newItems = [...currItems];
    

    Because both arrays still reference the same objects. So editing an object in one array edits it in both. You can project the array into a new array, and replace the value when found in that projection. For example, consider this:

    setCartItems(currItems => 
      const idx = currItems.findIndex(cartItem => cartItem.id === id);
      if (idx !== -1) {
        return currItems.map((item, i) => 
          i === idx ?
          { ...item, quantity: item.quantity + 1 } :
          item
        );
      }
      return [...currItems, { ...item, quantity: 1 }];
    });
    

    The idea here is that as map iterates over currItems it's specifically looking for the index you're targeting. For the item at that index it returns an entirely new item containing the same properties as the existing item, but with a new quantity value. For every other index of the array it just returns the existing item as-is.