Search code examples
javascriptreactjsreact-hooksreact-context

When increasing the item counter, it calculates the total amount of the items incorrectly


I'm working on a React project and using the useReducer hook to manage my state. I have a specific issue with updating the fixed price for my items.

In my code, I'm trying to increment the quantity of items in the shopping cart, and with each increment, I want to add the fixed price to the total price. However, I'm facing a problem where the fixed price keeps updating constantly.

Here's my code:

import React, { createContext, useContext, useEffect, useReducer } from 'react';
import { FetchedOrders } from "../../api/fetch"; 

// Створюємо контекст
const OrdersContext = createContext();
const OrdersDispatchContext = createContext();

function ordersReducer(orders, action) {
    switch (action.type) {
        case "ORDERS":
            return action.payload;
        case "REMOVE_ORDER":
            return orders.filter((order) => order.id !== action.id);
        case "INCREMENT": 
        const { id } = action;
            const updatedOrders = orders.map((order) => {
                if (order.id === id) {
                    const newAmount = order.amount + 1;
                    const newPrice = parseFloat(order.price) + parseFloat(order.price); 
                    return { ...order, amount: newAmount, price: newPrice };
                } else {
                    return order;
                }
            });
            return updatedOrders;
        default: return orders
    }
}

export function StateProvider({ children }) {
    const [orders, dispatch] = useReducer(ordersReducer, null);

    useEffect(() => {
        const fetching_orders = async () => {
            const data = await FetchedOrders();
            dispatch({ type: 'ORDERS',  payload: data});
        };
  
        fetching_orders();
    }, [])

    return (
        <OrdersContext.Provider value={{ orders }}>
            <OrdersDispatchContext.Provider value={{ dispatch }}>
                {children}
            </OrdersDispatchContext.Provider>
        </OrdersContext.Provider>
    );
};

export function useOrdersValues() {
    return useContext(OrdersContext);
}

export function useOrdersDispatch() {
    return useContext(OrdersDispatchContext);
};
import Card from '../card/card.jsx';
import { useOrdersDispatch, useOrdersValues } from '../context/context.js';

const Main = () => {
    const { orders } = useOrdersValues();
    const { dispatch }  = useOrdersDispatch();
 
    if (!orders) {
        return <div>Loading...</div>;
    }
    // delete order
    const removeOrders = (id) => {
         dispatch({type: "REMOVE_ORDER", id})
    }
    const arrowUp = (id,price) => {
         dispatch({type: "INCREMENT", id});
    }
    const arrowDown = (id) => {
         dispatch({type: "DECREMENT", id});
    }

    // render cards
    const render_orders = (arr) => {
        const list = arr.map((item) => {
          return <Card key={item.id} 
                       {...item} 
                       removeOrder={removeOrders}
                       counterUp={arrowUp}
                       counterDown={arrowDown}/>     
            
        })
    return list
}

const list_orders = render_orders(orders)
    return (
      <div>
        <h1>YOUR BAG</h1>
          {list_orders}
      </div>
    );
}
export default Main;
import "./card.scss";
import { FaAngleUp } from "react-icons/fa6"
import { FaAngleDown } from "react-icons/fa6"

const Card = ({id, img, price, title, amount, removeOrder, counterUp, counterDown}) => {
    return (
        <div>
            <div className="content_order">
                <div className="img_order">
                    <img src={img} alt="" />
                </div>
                <div>
                    <p className="title">{title}</p>
                    <p className="price">{price}</p>
                    <button onClick={() => removeOrder(id)}>remove</button>
                </div>
            </div>
            <div className="counter_bag">
                    <button onClick={() => counterUp(id)}><FaAngleUp /></button>
                       <span>{amount}</span>
                    <button onClick={() => counterDown(id)}><FaAngleDown /></button>
            </div>
        </div>
    )
}
export default Card;

The issue I'm experiencing is that the newPrice variable appears to update continuously when I increment the item quantity. My goal is to add the fixed price to the total price with each increment of the item counter.


Solution

  • Issue

    The INCREMENT reducer case is also updating the order price, each time doubling it.

    case "INCREMENT": 
      const { id } = action;
      const updatedOrders = orders.map((order) => {
        if (order.id === id) {
          const newAmount = order.amount + 1;
    
          // Next price is twice current order price
          const newPrice = parseFloat(order.price) + parseFloat(order.price); // <-- 2x
    
          return { ...order, amount: newAmount, price: newPrice };
        } else {
          return order;
        }
      });
      return updatedOrders;
    

    Solution

    Computing a total value from some quantity and cost value is often considered derived state and it's a bit of a React anti-pattern to store derived state in state. If you must compute and store a total price value in state then I suggest leaving the unit price alone and add a new total property to the state for the computed value.

    Example:

    case "INCREMENT": 
      const { id } = action;
      const updatedOrders = orders.map((order) => {
        if (order.id === id) {
          const newAmount = order.amount + 1;
    
          return {
            ...order,
            amount: newAmount,
            total: newAmount * Number(order.price), // <-- update total, not price
          };
        } else {
          return order;
        }
      });
      return updatedOrders;
    
    const Card = ({
      id,
      img,
      total, // <-- not price
      title,
      amount,
      removeOrder,
      counterUp,
      counterDown
    }) => {
      return (
        <div>
          <div className="content_order">
            <div className="img_order">
              <img src={img} alt="" />
            </div>
            <div>
              <p className="title">{title}</p>
              <p className="total">{total}</p> // <-- total, not unit price
              <button onClick={() => removeOrder(id)}>remove</button>
            </div>
          </div>
          <div className="counter_bag">
            <button onClick={() => counterUp(id)}><FaAngleUp /></button>
            <span>{amount}</span>
            <button onClick={() => counterDown(id)}><FaAngleDown /></button>
          </div>
        </div>
      );
    };
    

    However, as I stated, computing a total and storing it in the state is a React anti-pattern. Just update the item quantity and compute the derived item total when rendering.

    Example:

    case "INCREMENT":
      return orders.map((order) => order.id === action.id
        ? { ...order, amount: order.amount + 1 }
        : order
      );
    
    const Card = ({
      id,
      img,
      price,
      title,
      amount,
      removeOrder,
      counterUp,
      counterDown
    }) => {
      const total = amount * Number(price); // <-- compute total
    
      return (
        <div>
          <div className="content_order">
            <div className="img_order">
              <img src={img} alt="" />
            </div>
            <div>
              <p className="title">{title}</p>
              <p className="total">{total}</p> // <-- total
              <button onClick={() => removeOrder(id)}>remove</button>
            </div>
          </div>
          <div className="counter_bag">
            <button onClick={() => counterUp(id)}><FaAngleUp /></button>
            <span>{amount}</span>
            <button onClick={() => counterDown(id)}><FaAngleDown /></button>
          </div>
        </div>
      );
    };