Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

An update to the state from useState is not registered in event handler onTransitionEnd #14812

Closed
DennisSkoko opened this issue Feb 10, 2019 · 16 comments

Comments

@DennisSkoko
Copy link

Do you want to request a feature or report a bug?
Bug

What is the current behavior?
When updating the state from useState doesn't actually update the component. In this case it is being updated on an event handler. I used console.log to verify that it is being called yet no update in the component is being dispatched. It's like React doesn't register that it wants to update the state.

Might want to throw out that I'm new with this React Hooks and it could be something that I'm missing.

If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. Your bug will get fixed much faster if we can run your code and it doesn't have dependencies other than React.
I've setup a simple example showcasing my issue, I also included an implementation with the old class syntax and when doing it with classes, it works fine. Here is the link to the CodeSandbox

This example is taken from my project, I'm fading out and translating upwards so I want to keep the text until the animation is done.

What is the expected behavior?
The component should update with the new registered state when trying to update state in a event handler.

Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?
React v16.8
React DOM v16.8

@gaearon
Copy link
Collaborator

gaearon commented Feb 10, 2019

When you include an example, please write the exact reproduction instructions. It's never obvious what an example is supposed to show, what the expected behavior is, and where it deviates from the actual behavior. A list of steps to do would be very helpful!

@DennisSkoko
Copy link
Author

Apologies,
I've updated the example as well to make it easy to see the problem (hopefully)

When the page is loaded there is no message so the No message text is displayed. When clicking on the button the message becomes My message. When clicking again then the text should become No message after it has transitioned but it doesn't.

The console can also be checked, the actual is after click toggle message twice and waiting for the transition to end:

Console was cleared
Updated Object {currentMsg: null, stagedMsg: null}
Updated Object {currentMsg: "My message", stagedMsg: "My message"}
Updated Object {currentMsg: "My message", stagedMsg: null}
handleTransitionEnd Object {currentMsg: "My message", stagedMsg: null}

expected is:

Console was cleared
Updated Object {currentMsg: null, stagedMsg: null}
Updated Object {currentMsg: "My message", stagedMsg: "My message"}
Updated Object {currentMsg: "My message", stagedMsg: null}
handleTransitionEnd Object {currentMsg: "My message", stagedMsg: null}
Updated Object {currentMsg: null, stagedMsg: null}

The reason why I'm expecting another update is because I'm updating the state in the transition handler.

@gaearon
Copy link
Collaborator

gaearon commented Feb 10, 2019

Aren't you resetting it here?

  if (stagedMsg && stagedMsg !== currentMsg) setCurrentMsg(stagedMsg);

So even if currentMsg is null, it gets reset back to stagedMsg.

@DennisSkoko
Copy link
Author

It checks if the prop (aka stagedMsg) is not null so it doesn't get fired. The purpose of this is for two uses cases:

  • When currently no message is being displayed and the stagedMsg changes to a message, we want to update it immediately.
  • When a message is currently displayed and want to change it to another message (e.g. My message to My other message) then we do that immediately as well.

Checked just in case by adding a console.log to see when it get fired and this is the result

Console was cleared
Updated Object {currentMsg: null, stagedMsg: null}

// After first click
Change currentMsg immediately
Updated Object {currentMsg: "My message", stagedMsg: "My message"}

// After second click
Updated Object {currentMsg: "My message", stagedMsg: null}
handleTransitionEnd Object {currentMsg: "My message", stagedMsg: null}

@DennisSkoko
Copy link
Author

My guess is that when the following is executed, it somehow doesn't register any changes and does not update.

const handleTransitionEnd = () => {
  // Dirty hack to make sure this is only called when it
  // has transitioned to the empty state of the message
  if (!stagedMsg) {
    console.log("handleTransitionEnd", { currentMsg, stagedMsg });
    setCurrentMsg(null);
  }
};

Maybe worth noting, I have tried using timeout instead of onTransitionEnd but with no luck.

useEffect(() => {
  if (!stagedMsg && currentMsg) {
    setTimeout(() => {
      setCurrentMsg(null)
    }, 1000)
  }
})

@hreinhardt
Copy link

An easy fix seems to be changing this line:

if (stagedMsg && stagedMsg !== currentMsg) setCurrentMsg(stagedMsg);

to this:

useEffect(() => {
    if (stagedMsg && stagedMsg !== currentMsg) setCurrentMsg(stagedMsg);
});

Which makes the code work for me.


Doing some more research, I changed

if (!stagedMsg) {
    console.log("handleTransitionEnd", { currentMsg, stagedMsg });
    setCurrentMsg(null);
}

to this

if (!stagedMsg) {
    console.log("handleTransitionEnd", { currentMsg, stagedMsg });
    setCurrentMsg(x => {
        console.log("setCurrentMsg old value", x);
        return null;
    });
}

so that setCurrentMsg prints the previous value of the variable. It outputs the following:

handleTransitionEnd Object {currentMsg: "My message", stagedMsg: null}
setCurrentMsg old value: null

So apparently setCurrentMsg thinks the variable is already 'null', therefore not triggering a render; even though the console.log statement right before showed that the variable is not null but contains "My message".

@DennisSkoko
Copy link
Author

useEffect(() => {
    if (stagedMsg && stagedMsg !== currentMsg) setCurrentMsg(stagedMsg);
});

I don't want to do this, this will result in a render where the prop has changed and then another render because the state is being updated, in other words, the component will render twice. I want to mimic getDerivedStateFromProps and looked it up at the docs to see how to do it.

 if (!stagedMsg) {
   console.log("handleTransitionEnd", { currentMsg, stagedMsg });
   setCurrentMsg(x => {
       console.log("setCurrentMsg old value", x);
       return null;
   });
 }

I did not realise this, why it is null? I even use React developer tools and see that the state is not null.
It explains why there is no re-render but everything tells me that the state (except for the callback for setCurrentMsg()) is containing My message but the state is actually null somehow, which doesn't make any sense because my event handler is the only one that sets it to null.

@ioss
Copy link
Contributor

ioss commented Feb 11, 2019

I worked my way through this as @hreinhardt did, but changed the currentMsg to an object (which apart from probably being the simplest workaround) gave me the chance to see what the transitionEnd handler really "sees". (as different nulls are hard to discriminate ;) )
Here is the updated CodeSandbox: https://codesandbox.io/s/n7no7px18j

The result is: the transitionEnd handler will "see" a stale state: the one it set itself (or on the first iteration the initial state, which is also already stale).

To me this seems to be a bug.

(BTW: here is the cleaned up code with the workaround, that at least does what you expect: https://codesandbox.io/s/p31rlmrqrx)

@hreinhardt
Copy link

@ioss Very interesting. I agree your code strongly points at this being a bug.

Meanwhile, I tried finding a minimal example of this problem and ended up with this: https://codesandbox.io/s/7mxz7v8x0

@DennisSkoko
Copy link
Author

Thanks for the help, will have to use this solution from @ioss for now until bug has been fixed. Was really hard to decide if it was a bug from Facebook or just me being stupid and don't know how to use hooks :)

@ioss
Copy link
Contributor

ioss commented Feb 11, 2019

Just as a note, this issue was introduced in this commit (790c8ef04...), which introduced "bailout", if the reducer reduces to the previous (current) state. Which it correctly does.
The problem is, that the previous (current) state is stale / wrong in that transitionEnd handlers setState call.

I'd be happy to continue my research, but it might take some days, as I am pretty busy at the moment.

@DennisSkoko
Copy link
Author

@gaearon What should we do with this issue, do you still need more information?

I tried looking into the commit but it would take too much time for me to look into what introduced the issue. I feel like I should leave that to the experts instead. 😃

@hreinhardt
Copy link

I think it could be the same bug as in: #14849

So we should probably wait until that is fixed and see if the issue persists.

@Finesse
Copy link

Finesse commented Feb 21, 2019

I think I have the same issue: #14910

@ioss
Copy link
Contributor

ioss commented Feb 21, 2019

@gaearon
Copy link
Collaborator

gaearon commented Feb 21, 2019

Fixed in 16.8.3.
https://codesandbox.io/s/w50mm4kzw

@gaearon gaearon closed this as completed Feb 21, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants