8000 Call componentDidLeave after setState in ReactTransitionGroup by felipethome · Pull Request #7084 · facebook/react · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Call componentDidLeave after setState in ReactTransitionGroup #7084

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

Closed

Conversation

felipethome
Copy link
@felipethome felipethome commented Jun 20, 2016

If the developer uses the componentDidLeave hook to call setState() and remove the react transition group component, then the setState in _handleDoneLeaving() in ReactTransitionGroup.js will generate the warning:

Warning: setState(...): Can only update a mounted or mounting component.

To fix that we just need to place componentDidLeave() call after the setState in _handleDoneLeaving() so any setState called by the user will be placed in the queue after the setState in _handleDoneLeaving() or use isMounted().

@ghost
Copy link
ghost commented Jun 20, 2016

Thank you for your pull request and welcome to our community. We require contributors to sign our Contributor License Agreement, and we don't seem to have you on file. In order for us to review and merge your code, please sign up at https://code.facebook.com/cla - and if you have received this in error or have any questions, please drop us a line at cla@fb.com. Thanks!

@ghost
Copy link
ghost commented Jun 20, 2016

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Facebook open source project. Thanks!

@ghost ghost added the CLA Signed label Jun 20, 2016
@felipethome
Copy link
Author
felipethome commented Jun 20, 2016

To fix that we just need to place componentDidLeave() call after the setState in _handleDoneLeaving() so any setState called by the user will be placed in the queue after the setState in _handleDoneLeaving()

This approach makes one of the test cases fail so I'm using isMounted(). Is that because setState can be synchronous?

@aweary
Copy link
Contributor
aweary commented Jun 20, 2016

isMounted is considered an anti-pattern, I think a different approach would be more stable (though I'm not sure what that would be just yet).

If the developer uses the componentDidLeave hook to call setState() and remove the react transition group component, then the setState in _handleDoneLeaving() in ReactTransitionGroup.js will generate the warning:

Are you saying that in a child's componentDidLeave hook you're unmounting the parent ReactTransitionGroup? I'm just trying to understand the use case here.

@felipethome
Copy link
Author

Yes, I know and I first tried to solve the problem in a different way if you look my first commit, but unfortunately it fails for one test case because you can not be sure anymore that componentWillUnmount will be called after componentDidLeave (though I don't think that should be considered a problem).

isMounted() is considered an anti-pattern in the documentation, but it is already used in some test cases and in ReactCSSTransitionGroup.

@felipethome
Copy link
Author
felipethome commented Jun 20, 2016

Not in the child. I imagined this scenario: you use the componentDidLeave hook from the children to tell the parent that a child left. When the last child of your group left and your group is empty maybe you would like to remove the group from the DOM (the outer div/span/somethig else that wraps the children). So you call in your parent some state update that would do that for you. This would trigger the warning.

@aweary
< 8000 div class="ml-n3 timeline-comment unminimized-comment comment previewable-edit js-task-list-container js-comment timeline-comment--caret" data-body-version="7125d3110610056f1f74b2af66fd7454a8372484e2c8871acf5317af17194a5f">
Copy link
Contributor
aweary commented Jun 20, 2016

isMounted() is considered an anti-pattern in the documentation, but it is already used in some test cases and in ReactCSSTransitionGroup.

These are likely legacy implementations (The isMounted call in ReactCSSTransitionGroupChild.js was implemented in 2013), which IMO means it's not a good idea to implement new behavior based on a soon-to-be-deprecated API.

Not in the child. I imagined this scenario: you use the componentDidLeave hook from the children to tell the parent that a child left. When the last child of your group left and your group is empty maybe you would like to remove the group from the DOM (the outer div/span/something else that wraps the children). So you call in your parent some state update that would do that for you. This would trigger the warning.

I'm not extremely familiar with ReactTransitionGroup, can you tell me if this: https://jsfiddle.net/7hyr84Le/3/ matches the use case you're trying to support here?

@felipethome
Copy link
Author
felipethome commented Jun 20, 2016

These are likely legacy implementations

It is true. I still prefer to just call the componentDidLeave hook after setState.

I'm not extremely familiar with ReactTransitionGroup, can you tell me if this: https://jsfiddle.net/7hyr84Le/3/ matches the use case you're trying to support here?

Yes,but with one difference: you need to add this:

componentWillLeave: function (callback) {
  if (this.props.id === 0) {
    setTimeout(callback, 0);
  }
  else {
    callback();
  }
},

Otherwise React will not call the function passed to setState() in ReactTransitionGroup. (I think it is because of the difference between setState being synchronous or asynchronous depending how it was called). The reason why use timeouts it is exactly control the transition time. You will need that in a real application of the ReactTransitionGroup.

@felipethome
Copy link
Author

At least for this case we could also use componentWillUnmount to flag if setState can be called or not.

@aweary
Copy link
Contributor
aweary commented Jun 20, 2016

Thanks for clarifying, I can see the behavior you're describing. This seems to represent the exact anit-pattern that isMounted is being deprecated to avoid, externally queueing setState calls that might be called after a component has unmounted.

Is there a reason you want to completely unmount ReactTransitionGroup? What if you ensured that callback was invoked before you unmounted, by moving the function call that unmounts it to the setTimeout all?

 componentWillLeave: function (callback) {
  if (this.props.id === 0) {
    setTimeout(() => {
     callback()
     this.props.onAllChildrenLeft()
    }, 0);
  }
  else {
    callback();
  }
}

This would make sure _handleDoneLeaving is called before you unmount the parent TransitionGroup

https://jsfiddle.net/yzzd3em3/1/

@felipethome
Copy link
Author
felipethome commented Jun 20, 2016

This seems to represent the exact anit-pattern that isMounted is being deprecated to avoid, externally queueing setState calls that might be called after a component has unmounted.

Maybe it is the opposite, it is because the setState() is flushed right away that the warning happens, I think that is the behavior when using any kind of update that is not related to Synthetic Events.

What if you ensured that callback was invoked before you unmounted, by moving the function call that unmounts it to the setTimeout

That solves the warning, but adding a timeout to the setState that removes the group also does:

onAllChildrenLeft: function() {
  var _this = this;
  setTimeout(function () {
    _this.setState({ showGroup: false })
   }, 0);
},

We could use componentWillUnmount instead of componentDidLeave too, but that wasn't what I had in mind when I opened the pull request. In both of our approaches, the developer needs to know the internals of ReactTransitionGroup to understand what he is doing. Looking at the API would make perfect sense to add the callback in componentDidLeave(), I mean, why add your callback in componentWillLeave when you want to know if a child left? But, the problem with the componentDidLeave() callback is that is being called before the component actually leaves.

Is there a reason you want to completely unmount ReactTransitionGroup?

Well, we could use display:none, but would be preferable to remove the entire node so React can just ignore it. Again, when I opened the pull request I wasn't looking for a solution for this problem, but for a fix for something that IMHO is an API issue.

@aweary
Copy link
Contributor
aweary commented Jun 20, 2016

Maybe it is the opposite, it is because the setState() is flushed right away that the warning happens, I think that is the behavior when using any kind of update that is not related to Synthetic Events.

I'm not entirely sure what you mean, the synthetic event system doesn't have a direct connection to how React manages setState calls. setState will eventually call enqueueSetState which queries the internal instance for the component. If the instance doesn't exist (indicating it's likely been unmounted) then it logs the warning you mention originally and returns early, making it a no-op.

You get the warning because you're pushing the setState call to the top of the call stack using setTimeout meaning it's called after you're unmounting the parent component. I'm not sure this is a pattern that should necessarily be supported and I certainly don't think that using isMounted is the right answer here given its status.

I'm interested in what some of the React team thinks (cc @jimfb @spicyj)

8000
@felipethome
Copy link
Author
felipethome commented Jun 20, 2016

setState will eventually call enqueueSetState which queries the internal instance for the component. If the instance doesn't exist (indicating it's likely been unmounted) then it logs the warning you mention originally and returns early, making it a no-op.

I don't have knowledge about the internals to know what is happening so I will not start a discuss here, this is more like a guess and a question at the same time. I just read (don't have a really good source to point here) that setState calls happening because of an (synthetic) event handler are batched, while the ones happening because of a setTimeout or other asynchronous function need to be flushed right away.

I'm not sure if I get it what you mean by "top of the call stack". In my mind is like that:

  1. ReactTransitionGroup calls componentWillLeave
  2. componentWillLeave set a timeout with the duration of the transition
  3. In the end of this timeout the callback passed to componentWillLeave is called. And this callback is: ReactTransitionGroup._handleDoneLeaving
  4. _handleDoneLeaving starts by calling the componentDidLeave method
  5. Now the user performs a setState that removes the group component
  6. The flow returns to _handleDoneLeaving (still the same javascript round) and calls setState

The problem for me is that the setState in 5 happens before the setState in 6.
The approach I'm describing with the timeouts is also the same approach of ReactCSSTransitionGroup

I certainly don't think that using isMounted is the right answer here given its status

I also don't like it. I pointed another two ways of solving it, one using componentWillUnmount and the other moving the componentDidLeave call to the bottom of _handleDoneLeaving.

@aweary
Copy link
Contributor
aweary commented Jun 21, 2016

I don't have knowledge about the internals to know what is happening so I will not start a discuss here, this is more like a guess and a question at the same time. I just read (don't have a really good source to point here) that setState calls happening because of an (synthetic) event handler are batched, while the ones happening because of a setTimeout or other asynchronous function need to be flushed right away.

I'm pretty sure that's not how state updates are queued. setState calls are enqueued in ReactUpdateQueue. There's no difference between calling setState in a timeout call or in an event handler (cc @jimfb correct me if I'm wrong). When you call setTimeout you're basically saying that the action should be delayed, so it processes everything else already queued up, which in this cases includes the call to unmount the ReactTransitionGroup component.

After it does that, it processes your setState call which fails because ReactTransitionGroup was just unmounted. Thats the issue with calling setState in an async function, it runs the risk of being called after the component has mounted. That's why its typically recommend to clear any timeouts in componentWillUnmount.

@felipethome
Copy link
Author

About the setState OK, you must know better than me how his implementation works, and does not affect this pull request directly.

After it does that, it processes your setState call which fails because ReactTransitionGroup was just unmounted. Thats the issue with calling setState in an async function, it runs the risk of being called after the component has mounted. That's why its typically recommend to clear any timeouts in componentWillUnmount.

You are misunderstanding the workflow. What you described is not my architecture:

  1. ReactTransitionGroup calls componentWillLeave
  2. componentWillLeave set a timeout with the duration of the transition
  3. In the end of this timeout the callback passed to componentWillLeave is called. And this callback is: ReactTransitionGroup._handleDoneLeaving
  4. _handleDoneLeaving starts by calling the componentDidLeave method
  5. Now the user performs a setState that removes the group component
  6. The flow returns to _handleDoneLeaving in ReactTransitionGroup (still the same javascript round) and calls setState

The setState giving the warning is not mine, is from ReactTransitionGroup. You should give a look at how the ReactCSSTransitionGroup is implemented. We should let the others give a look because we already discussed too much in this pull request and it is becoming difficult to read.

@gaearon
Copy link
Collaborator
gaearon commented Jun 26, 2016

Generally, if a function calls setState, we should make sure that wherever this function is queued (e.g. interval), it is removed from that queue if the component unmounts. We should not be using isMounted() for that.

Is there a reason we can’t do this here? I’ll try to take a closer look later today but that’s the general guideline.

@gaearon
Copy link
Collaborator
gaearon commented Jun 26, 2016

Could #6710 be another symptom of the same problem?

@felipethome
Copy link
Author
felipethome commented Jun 26, 2016

@gaearon the setTimeout is cleaned in the componentWillUnmount(). The problem is not that. When _handleDoneLeaving() is executed means the timeout is not in the queue anymore because it just got dispatched. In other words, the callback passed to componentWillLeave(callback) (remember that componentWillLeave() is where the timeout is created) is in fact: ReactTransitionGroup._handleDoneLeaving() function.

You can reproduce the problem we are talking about using the ReactCSSTransitionGroup adding some lines of code to this component.

@felipethome
Copy link
Author

I don't think #6710 is the same problem, because there he is checking if component exists and component is one of the child passed to ReactTransitionGroup. Here the component that is being removed is the instance of ReactTransitionGroup and not one of his children.

@gaearon
Copy link
Collaborator
gaearon commented Jun 27, 2016

Would delaying component.componentDidLeave() until after setState solve the problem? Same for componentDidEnter().

@felipethome
Copy link
Author

If you are taking about the _handleDoneLeaving() It doesn't solve. That is what I tried before, but it fails the test cases because you can not guarantee anymore that componentWillUnmount() will be called after componentDidLeave(). And this order is something that the test cases takes into consideration. You also would need to have different approaches for when the component really will leave and when he was supposed to leave, but he entered again (in the source code of _handleDoneLeaving() in ReactTransitionGroup this is the "if" case), otherwise you would call component.componentWillEnter() before component.componentDidLeave()

@gaearon gaearon added this to the 15-next milestone Jun 27, 2016
@gaearon
Copy link
Collaborator
gaearon commented Jun 27, 2016

Eh. I’m going to tentatively accept it because:

  • This fixes an annoying warning.
  • We don’t like isMounted but we don’t really plan to maintain ReactTransitionGroup anyway and we intend to hand it over to the community.
  • We already use isMounted in ReactCSSTransitionGroupChild anyway.

So unless @spicyj or @zpao have reservations about this I’m going to merge it in a few days.

@aweary
Copy link
Contributor
aweary commented Jun 27, 2016

This fixes an annoying warning.
We don’t like isMounted but we don’t really plan to maintain ReactTransitionGroup anyway and we intend to hand it over to the community.
We already use isMounted in ReactCSSTransitionGroupChild anyway.

I feel like none of those are really great justifications. Using isMounted to get rid of warnings is treating the symptoms not the issue, using a nearly-deprecated API in a project that will be handed off to the community will just make it harder for the community to maintain/fix, and appealing to existing implementations seem backwards since they're essentially legacy implementations.

This issue boils down to the fact that the API allows/encourages enqueing a setState call in a timeout, but doesn't consider that the user might completely unmount the component before that happens. Is that a pattern that should be encouraged? If so the API should be augmented to try and address this use case in a more future-proof way.

Could callback take an optional argument that tells ReactTransitionGroup whether it should attempt to update its children in state, and default to true? Or is there somewhere else in the component's lifecycle where the setState call would make more sense?

@gaearon
Copy link
Collaborator
gaearon commented Jun 27, 2016

using a nearly-deprecated API

Good point. I wasn’t aware isMounted is going to be deprecated in createClass-based classes but it appears so from #5465. I guess this doesn’t work then so I’ll un-accept.

This issue boils down to the fact that the API allows/encourages enqueing a setState call in a timeout, but doesn't consider that the user might completely unmount the component before that happens. Is that a pattern that should be encouraged? If so the API should be augmented to try and address this use case in a more future-proof way.

IMO “fake” lifecycle hooks like componentDidEnter is a bad API in the first place 😉 . For example, this doesn’t work with functional components or with higher order component wrappers. So I think the whole API of TransitionGroup is flawed, not just this one particular pattern, and there is no way to repair it without creating something completely different, at which point it might as well become a different package. We don’t plan to address this because we just don’t use this code. We can’t be good stewards of it.

@gaearon gaearon removed this from the 15-next milestone Jun 27, 2016
@gaearon
Copy link
Collaborator
gaearon commented Jun 27, 2016

In this case I’m closing this particular PR.
We can’t use isMounted() because it will get deprecated per #5465.

You are welcome to look for another way to solve this problem, but I think at this point it might be easier for you to fork ReactTransitionGroup and change how it works for your use case. We just don’t have the capacity to review pull requests to it because nobody at Facebook actively uses it. Sorry about this!

@gaearon gaearon closed this Jun 27, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants
0