React: `useState` hook behaves differently than old state approach when using callbacks

Created on 12 Jun 2019  路  4Comments  路  Source: facebook/react

I think I found a bug in a useState hook - at least it behaves in a different way than old state approach.

In the following code, using old state, when I hover and leave a button I will see foo variable set to 'foo' in the console:

https://codesandbox.io/s/ovssp

On the other hand, In this code, using useState hook, when I hover and leave a button I will see foo variable set to 'initial' in the console:

https://codesandbox.io/s/dxuse

I know, that the example does not make much sense, but I wanted to provide the simplest thing that can reproduce the issue. I faced this issue using Mapbox API.

Most helpful comment

@lesniakania, year @tgandrews is right. You can check this post to make it cleaner. Also you can try to use ref:
Edit zealous-maxwell-dwl4s

All 4 comments

Your function setup is closing over the value of state so the mouseout is attached contains the first value of foo.

My suggestion is to use the native react events on the element or use useEffect to manage these events as these jQuery events are side effects.

@lesniakania, year @tgandrews is right. You can check this post to make it cleaner. Also you can try to use ref:
Edit zealous-maxwell-dwl4s

jQuery was just an example of usage, I'm using Mapbox events, as I mentioned, so I cannot use native React events. I can use ref, I was just surprised that it works differently with the old state approach.

For your example, it is important to understand the how hooks work exactly (I suggest you go through this blog post for detailed information on how useEffect works: https://overreacted.io/a-complete-guide-to-useeffect/).

To answer your question, the callback function that you have provided setup is executed after the component is rendered. Each functional component receives it's own state variables declared with useState. Which means, after the first render, the value of foo is initial. Then your useEffect runs, I understand you are trying to imitate the componentDidMount here by not passing the empty dependency list. So your callback setup runs and attaches event handler to the button.

When the mouseover is executed, and you update the state, there is an update and as expected the component will be rendered. But you have to see here that since you have not provided the a unique key to the button, React will not find any difference in the DOM, hence there will be no DOM node updates. As a result, the mouseleave function that you expect to log new value of state, has still the old value that it got during the useEffect (think closures). This is the reason you still see initial printed in the logs.

To witness it, try adding a key to the button component, a random value (e.g. Math.random()) which will change at each render. You'll see that there is no log. The reason of this is, even before the mouseleave gets the chance to execute, the element is destroyed and replaced with a new element.

In the class components, things work in a different manner. There is an instance of class and you are able to refer it with this. In this case, each time you do mouseover and mouseleave, you are updating the state with foo and logging the value. But the key here is, you have this and it is able to refer to the latest value. The closure is not applicable here because you are accessing the state variable from this.

Add key to the button, and once again you won't find any logs for the same reason that each time there will be new rendered elements and hence mouseleave callback will be lost.

Hope this helped you to understand. So, it's not a bug. It's the way it has been designed.

Was this page helpful?
0 / 5 - 0 ratings