React input value not re-rendering on button change

Viewed 1372

I am building a shopping cart project.

One of the features that I am trying to build for my Cart page is an input value displaying the quantity for each product in the cart (so that the user can manually adjust the number of products) as well as a button either side of the input to decrement/increment the quantity by 1.

Here is a link to my codesandbox, and the issue can be recreated by adding one of any item to the cart:

https://codesandbox.io/s/yqwic

The logic I have built for manually adjusting the input field works as intended, however when I click the increment/decrement buttons it is only adjusting the number of items in the cart display in the navbar. The input field is not updating - however when looking at the React-Developer-Tools components debugger, the underlying quantity is updating to the correct figure - it is just not rerendering the input value in the DOM.

Can anyone point out where I might be going wrong? I tried switching defaultValue to value in the input field however this prevents me from manually adjusting the input.

Thanks

2 Answers

You need to do 2 things,

<Quantity type='number' min='1' value={quantity} ref={itemRef} onChange={handleChange} />
  1. Add the value prop to keep your input in sync with your context value.

  2. Replace the onBlur with onChange . You need to do this to make sure that changing the values within the input updates your context value.

Issue

  1. You are severely mutating your cart items state in the AppLogic utilities.
  2. You are rendering an "uncontrolled" input for manual input. This means it initially renders with a value, but as you externally change the "state" the value represents it won't update since it's uncontrolled.

Solution

AppLogic.js

Fix your reducer utilities so they aren't mutating cart items. When updating them you should shallow copy the entire cart items array and then shallow copy the specific cart item.

export const checkIfItemInCart = (item, array, sum) => {
  const index = array.findIndex((object) => object.id === item.id);
  if (index !== -1) {
    const newArray = array.map((item, i) => i === index ? {
      ...item,
      quantity: item.quantity + (sum === "add" ? 1 : -1)
    } : item)
    if (!newArray[index].quantity) {
      return deleteItemLookup(item, newArray);
    }
    return newArray;
  }
  return array.concat(item);
};

export const customQuantityUpdate = (item, array, newQuantity) => {
  const index = array.findIndex((object) => object.id === item.id);

  const oldQuantity = array[index]?.quantity;

  // This could be more DRY, but edited to work
  if (newQuantity > oldQuantity) {
    return { array: array.map((item, i) => i === index ? {
      ...item,
      quantity: newQuantity
    } : item), type: "add", newQuantity, oldQuantity };
  } else if (newQuantity < oldQuantity) {
    return { array: array.map((item, i) => i === index ? {
      ...item,
      quantity: newQuantity
    } : item), type: "subtract", newQuantity, oldQuantity };
  } else {
    return { array, type: "", newQuantity, oldQuantity };
  }
};

CartItem.js

Use the value attribute and onChange prop to convert the input to a fully controlled input.

<Quantity
  type="number"
  min="1"
  value={quantity}
  ref={itemRef}
  onChange={handleChange}
/>

Demo

Edit react-input-value-not-re-rendering-on-button-change

Related