4

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

Drew Reese
  • 165,259
  • 14
  • 153
  • 181

2 Answers2

4

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.

Shyam
  • 5,292
  • 1
  • 10
  • 20
  • Thanks. Is there a reason that onBlur can't be used to achieve your second point ? – LearningPython Jun 11 '21 at 07:37
  • I think I have just understood why from an error provided by react: ```Warning: You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`.``` – LearningPython Jun 11 '21 at 07:38
  • However, react also suggests I should **not** be using both `value` and `defaultValue` - ```arning: styled.input contains an input of type number with both value and defaultValue props. Input elements must be either controlled or uncontrolled (specify either the value prop, or the defaultValue prop, but not both). Decide between using a controlled or uncontrolled input element and remove one of these props. More info: https://reactjs.org/link/controlled-components``` – LearningPython Jun 11 '21 at 07:39
  • Update: removing `defaultValue` has solved the issue. – LearningPython Jun 11 '21 at 07:41
  • 1
    Right, you can't specify both the `defaultValue` and `value` attributes on an input. – Drew Reese Jun 11 '21 at 07:42
  • 1
    You should just use `value` here and can safely remove the defaultValue . And also since your component is synced with the value from the context . onBlur will fire only when the component loses its focus . So if you change from 1- 5 and click outside you are firing the dispatch with the final value - `5` and the value change from 1-4 will not reflect in the UI as you are not dispatching. so `onChange` is needed here to capture your every change event . – Shyam Jun 11 '21 at 07:45
2

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

Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • Thanks Drew. This was my first time working with `useReducer`, I am pleased you picked up on it. I will review and address. – LearningPython Jun 11 '21 at 08:03
  • Just some comments to follow up on the first part of this: `const index = array.findIndex((object) => object.id === item.id);` returns `undefined` rather than `-1` if the item is not in the array. Additionally, I needed to change ` if (!newArray[index].quantity)` to ` if (!newArray[index]?.quantity)` to prevent an undefined error being thrown. I'm still working through the rest. – LearningPython Jun 11 '21 at 08:33
  • 1
    @LearningPython `Array.prototype.findIndex` certainly returns `-1` if no element matches the predicate. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/findIndex It's `Array.prototype.find` that can return undefined. You're absolutely correct about the second bit though, there should be a null-check on `newArray[index]` before accessing properties. Updated my answer. – Drew Reese Jun 11 '21 at 08:35
  • good spot, I had erroneously used `find` rather than `findIndex`. Thanks. – LearningPython Jun 11 '21 at 08:39
  • Regarding the CartItem.js, this all works fine, except if I delete the number in the input (so it's just showing a blank input with a cursor) I receive the following error: `The specified value "NaN" cannot be parsed, or is out of range.`. Is there an easy way to prevent the number being manually deleted? – LearningPython Jun 11 '21 at 09:08
  • 1
    @LearningPython I don't know if you're using a different browser than me, but I'm unable to reproduce completely deleting the value in the input. Never the less, you can enforce a minimum value in the `onChange` handler with `const newQuantity = Math.max(1, Number(e.target.value) || 1);` if you are somehow able to input something other than a number (which results in falsey `NaN`) or a value less than the minimum. I updated my codesandbox. – Drew Reese Jun 11 '21 at 15:08
  • I ignore all uncontrolled warning previously, now i know the important of them.. thanks buddy, this solution really helpful.. – metalheadcoder Oct 17 '22 at 05:56