3

I am somewhat new to React and I am running into an issue and I was hoping someone will be willing to help me understand why my method is not working.

I have this state:

const [beers, setBeers] = useState([
    {
      id: 8759,
      uid: "8c5f86a9-87bf-41fa-bc7f-044a9faf10be",
      brand: "Budweiser",
      name: "Westmalle Trappist Tripel",
      style: "Fruit Beer",
      hop: "Liberty",
      yeast: "1056 - American Ale",
      malts: "Special roast",
      ibu: "22 IBU",
      alcohol: "7.5%",
      blg: "7.7°Blg",
      bought: false
    },
    {
      id: 3459,
      uid: "7fa04e27-0b6b-4053-a26b-c0b1782d31c3",
      brand: "Kirin",
      name: "Hercules Double IPA",
      style: "Amber Hybrid Beer",
      hop: "Nugget",
      yeast: "2000 - Budvar Lager",
      malts: "Vienna",
      ibu: "18 IBU",
      alcohol: "9.4%",
      blg: "7.5°Blg",
      bought: true
    }]

I am rendering the beers with a map function and I have some jsx that calls a handleClick function

 <button onClick={() => handleClick(beer.id)}>
        {beer.bought ? "restock" : "buy"}
      </button>

this is the function being called:

 const handleClick = (id) => {
 
   setBeers((currentBeers) =>
      currentBeers.map((beer) => {
        if (beer.id === id) {
          beer.bought = !beer.bought;
          console.log(beer);
        }
        return beer;
      })
    );
    
  };

I wanted to use an updater function to update the state, I am directly mapping inside the setter function and since map returns a new array, I thought everything would work correctly but in fact, it doesn't. It works only on the first button click and after that it stops updating the value.

I noticed that if I use this method:

  const handleClick = (id) => {
    const newbeers = beers.map((beer) => {
      if (beer.id === id) {
        beer.bought = !beer.bought;
      }
      return beer;
    });

    setBeers(newbeers);
  };

Then everything works as expected.

Can someone help me understand why my first method isn't working?

Why u do dis
  • 418
  • 4
  • 15
  • 1
    I created a [sandbox](https://codesandbox.io/s/brave-thunder-q258h?file=/src/App.tsx) of your code above as this was troubling me too. It seems to be working correctly for me. Can you please check? – Stuart Nichols Jan 01 '22 at 15:08
  • 1
    I can think of few reasons why your previous code is not working, but I have to see how you render the list of beer first to pinpoint. But the sandbox Stuart provide seems to work fine. Did you do anything differently? – Doppio Jan 01 '22 at 16:17
  • Hi @Doppio thank you for looking into this as well, here is the link: https://codesandbox.io/s/quirky-elion-4ke2v?file=/src/App.js – Why u do dis Jan 01 '22 at 20:01
  • Hi @StuartNichols, I indeed your example seems to be working, It looks almost the same Like what I did here https://gr.pinterest.com/pin/708754060108151596/. Thank you for taking the time and sending the sandbox link! – Why u do dis Jan 01 '22 at 20:48
  • 2
    I still can't explain why it doesn't work, but I can pinpoint the part that breaks is the `beer.bought = !beer.bought; return beer;` line. if you change code into `return { ... beer, bought: !beer.bought }` then it works. While I don't have a proper explanation, I know that we shouldn't update / mutate value of Array.map element (so beer.bought = xxxxx is a no-no way) – Doppio Jan 01 '22 at 21:05
  • Thanks a ton guys!! I had no idea that the objects inside needed to be cloned as well, I thought React will update as long as you pass a new array! Lesson learned! Thank you again for spending your time to look into this! – Why u do dis Jan 01 '22 at 21:36

2 Answers2

3

OK, I think I have figured it out. The difference between my sandbox and your sandbox is the inclusion of <StrictMode> in the Index file. Removing this fixes the issue, but is not the correct solution. So I dug a little deeper.

What we all missed was that in your code you were modifying the previous state object that is passed in. You should instead be creating a new beer object and then modifying that. So this code works (I hope):

setBeers((currentBeers) => 
  currentBeers.map((currentBeer) => { // changed beer to currentBeer
    const beer = {...currentBeer};
    if (beer.id === id) {
      beer.bought = !beer.bought;
    }
    return beer;
  )
});

I hope that this helps.

Stuart Nichols
  • 1,026
  • 1
  • 8
  • 11
  • 1
    One further thing on this is that I have found out that in development StrictMode causes the component to render twice. As your modification is a boolean toggle, I think that the state was updating, but it was updating twice which was appearing as if no change was made at all. [Here's a link](https://stackoverflow.com/questions/61578158/why-does-usestate-cause-the-component-to-render-twice-on-each-update). I learned something today. – Stuart Nichols Jan 01 '22 at 21:26
  • Thank you Stuart! I learned 2 things today, I always wondered why I always saw double console.logs, well now I know :)). Thank you for your help – Why u do dis Jan 01 '22 at 21:43
1

react does not deeply compares the object in the state. Since you map over beers and just change a property, they are the same for react and no rerender will happen. You need to set the state with a cloned object.

e.g.:

import {cloneDeep} from 'lodash';

...

    setBeers(
      cloneDeep(currentBeers.map((beer) => {
          if (beer.id === id) {
            beer.bought = !beer.bought;
            console.log(beer);
          }
          return beer;
        })
      )
    );
Simon Hansen
  • 622
  • 8
  • 15
  • Thank you so much Simon, You are absolutely correct, I had to clone the object instead of directly mutating it. I accepted an answer that used the spread operator instead of lodash but you are absolutely a star! thanks again for your time! – Why u do dis Jan 01 '22 at 21:45