RGog
RGog

Reputation: 76

cart count is not getting updated properly

screeen record of the issue: https://streamable.com/ofn42v

it is working fine in local but once deployed to production(vercel), it is not working. i have tried sooo many different things like having a separate state in cart, useEffect with totalQuantity in dependency array and nothing seems to work. Ideally when the totalQuantity inside the context is updated, the components using it should rerender as mentioned in react doc which is happening from n to 2 except for 1. can someone please help :(

my code for the cart icon in nav bar:

function Cart(props) {
  const { enableCart, totalQuantity } = useContext(AppContext);

  return (
    <>
      {enableCart ? (
        <Link href="/cart" passHref>
          <a aria-label="Shopping cart" title="Shopping cart">
            <Badge count={totalQuantity} offset={[0, 5]}>
              <ShoppingCartIcon className="w-7 h-7" />
            </Badge>
          </a>
        </Link>
      ) : null}
    </>
  );
}

Update quantity - code in appContext:

import { useCookies } from "react-cookie";
export const AppProvider = (props) => {

  const [cartItems, updateCart] = useState([]);
  const [totalQuantity, setTotalQuantity] = useState(0);
  const [cookies, setCookie] = useCookies(["cart"]);
  const cookieCart = cookies.cart;

  useEffect(() => {
    cartOperations();
  }, []);

  const calculateAmountQuantity = (items) => {
    let totalCount = 0;
    let totalPrice = 0;
    items.forEach((item) => {
      totalCount += item.quantity;
      totalPrice += item.price * item.quantity;
      setTotalAmount(totalPrice);
      setTotalQuantity(totalCount);
    });
  };

  const cartOperations = async (items) => {
    if (items !== undefined) {
      updateCart([...items]);
      calculateAmountQuantity(items);
    } else if (cookieCart !== undefined) {
      updateCart([...cookieCart]);
      calculateAmountQuantity(cookieCart);
    } else {
      updateCart([]);
      setTotalAmount(0);
      setTotalQuantity(0);
    }
  };
  
  const addItem = (item) => {
    let items = cartItems;
    let existingItem;
    if (items) existingItem = items.find((i) => i.id === item.id);

    if (!existingItem) {
      items = [
        ...(items || []),
        Object.assign({}, item, {
          quantity: 1,
        }),
      ];
      updateCart([...items]);
      setTotalAmount(totalAmount + item.price * 1);
      setTotalQuantity(totalQuantity + 1);
    } else {
      const index = items.findIndex((i) => i.id === item.id);
      items[index] = Object.assign({}, item, {
        quantity: existingItem.quantity + 1,
      });
      updateCart([...items]);
      setTotalAmount(totalAmount + existingItem.price);
      setTotalQuantity(totalQuantity + 1);
    }
    saveCartToCookie(items);
    saveCartToStrapi(items);
  };

i am storing the cart content in cookie.

code for AppContext is here in github, full nav bar code

Live url: https://sunfabb.com

Goto Products, add few items to cart, then try removing one by one from the cart page. (i have enabled react profiler in prod as well)

EDIT: This issue is completely specific to antd library. I was able to debug further based on the below 2 answers and there is nothing wrong with react context or re-render. i tried using a custom badge for cart and it is working perfectly fine. Yet to fix the antd issue though. I can go with custom one, but antd's badge is better with some animations.

Upvotes: 2

Views: 1755

Answers (2)

Noam Yizraeli
Noam Yizraeli

Reputation: 5394

From looking at the renders and from seeing that after a refresh the cart shows as empty as should be, it's probably a lifecycle issue.

I'd suggest creating another useEffect hook that listens to totalQuantity or totalAmount (logically the bigger of the two though by the state values it looks either should be fine) and in the hook call change the cart icon based on the updated sum

EDIT:

misread your inter-component imports, because Cart (from components/index/nav.js) should listen for changes from the context.provider you would use a context.consumer on Cart with the totalQuantity value (not just with importing the variable from the context as that rides on the application rendering from other reasons)

see example in consumer docs and in this thread, and check this GitHub issues page for other's detailed journey while encountering this issue more directly

Upvotes: 1

tperamaki
tperamaki

Reputation: 1028

As pointed out by @hackape, when setting the value of state to something that depends on the previous value of that state, you should pass a function to the setState instead of a value.

So instead of setTotalQuantity(totalQuantity + 1);, you should say setTotalQuantity(previousQuantity => previousQuantity + 1);.

This is the safe way of doing that, so for example if we are trying to do it twice simultaneously, they both get taken into account, instead of both using the same initial totalQuantity.

Other thing that I would think about changing is that you are setting those quantities and amounts in multiple places, and relying on the previous value. So if it goes out of sync once, it's out of sync also on the next action, and so on.

You could use the useEffect hook for this. Every time the cartItems change, calculate those values again, and do that based only on the new cartItems array, not on the old values.

Something like this for example:

useEffect(() => {
  setTotalAmount(cartItems.reduce((total, currentItem) => total + (currentItem.price * currentItem.quantity), 0));
  setTotalQuantity(cartItems.reduce((total, currentItem) => total + currentItem.quantity, 0));
}, [cartItems]);

Or if you prefer calling it like you do now, I would still replace the value with the reduce from my example, so it get's calculated based on the whole cart instead of previous value.

A shopping cart is usually something that contains less than 100 entries, so there is really no need to worry about the performance.

Upvotes: 1

Related Questions