-
Notifications
You must be signed in to change notification settings - Fork 47k
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
isMounted is not enough to know if you can setState #2787
Comments
Just to add to the confusion: |
+1 not to the original issue by to @dmitrig01's comment. |
We explicitly say in the documentation that the goal of So it's not right that it doesn't work in all cases |
Can I question the point of
|
Or, a |
Yeah, |
+1 Either way, |
I imagine setStateSafe would use some internal logic to determine what's safe or not, rather than using isMounted. It could even just fail silently rather than throwing an error like it currently does. There might be a use case for an unsafe setState, but I can't think of one - it's certainly the more unlikely case. |
I think in general, React has a pattern of blowing up in your face when you use it wrong (case in point, setState when a component is not mounted, but there are plenty of others) – which is in general good. However, in some cases, if you know what you're doing, it would be nice to suppress these errors. I would see the distinction as being - |
@sbrandwoo My understanding is that you're supposed to use componentWillUnmount to clean up any timers, cancel async requests, etc. Failure to cleanup is often a code smell that indicates a memory leak or other even more severe bug. That said, in the case of an asynchronous callback, it's easy to imagine why it would be easier to just ignore the update rather than attempt to cancel the callback, and this is where isMounted could come in as a handy guard. Once a component is unmounted, I think it is effectively dead. I don't think it can be re-mounted (at least until opaque prerendered components are supported). There really shouldn't be a reason to update it anymore, so doing so would strongly imply a bug. Having said that, I don't see why you couldn't set the state on an unmounted component, and the expected behavior is that it would effectively be a noop. My intuition would be to remove this error check and make setState and forceUpdate be valid at any time, maybe be a warning if you have findbugs turned on. I suspect @sebmarkbage has strong opinions on this topic. |
@JSFB to provide a concrete example of my use case, I have a "timer" component which counts down from 30 seconds to 0. I have one method to create an interval, and another to clear the interval. I call the method to clear the interval after the 30th tick and on componentWillUnmount. In the first case, I want to do a |
In I wrote a mixin, but due to limited access to internals, I am not happy with the result: |
Yea, there's no reason for this error to exist other than enforcing that you properly clean up your components. We could just as well ignore the value. I think that we'll likely make this a warning or at least make sure that it doesn't break reconciliation if it throws. I would like to deprecate There are three separate scenarios in play here:
This is generally a really bad idea. This is a race condition that will act differently depending on the timing of your system, so testing won't reveal scenarios that an end user will hit. If you write in a completely immutable style, it's not as bad, but still considered bad practice. If you have cached values, that you would like to use instead of causing two renders, you can at least build a separate synchronous API for that. E.g. in getInitialState: Additionally, in the current server rendering model, it's best practice to trigger async side-effects in However, this is mostly a non-issue if (3) is solved since
This pattern seems a bit unnecessary to me. Unmounting and and reaching completion is two different concepts and are usually different code paths. Even if they do the same thing, it might be worth separating them to make refactoring easier when one changes. I don't see this as a major issue that we even need to error in production, we could warn in dev. It doesn't suffer from the same consequences as (3). So we could even drop the warning. However, the question is: Is it more commonly an mistake or a legit pattern? If it's a legit pattern, then we should not warn since warnings are meant to be cleaned up. Excessive warnings creates apathy. However, even if it is legit, should we warn anyway because the majority case is a mistake? I'm not sure. I could go either way. I don't think that this is a common mistake but not sure.
This is a strong indication that an asynchronous callback isn't being properly cleaned up. Unfortunately, mainstream JS APIs makes it very easy to avoid cleaning up hanging asynchronous callbacks. One callback isn't a big deal. However, that callback hangs on to objects and intermediate callbacks, promises and subscriptions. If alot of your components do this, you will quickly run into memory issues. This is a case of death by a thousand cuts. There is always a reason to avoid the complexity of cleanup logic, so you end up convincing yourself that it's ok for your special case. Then you run into GC and memory issues, and blame React for being slow. This is especially critical on mobile devices where memory issues can cause a browser to crash. There are two types of callbacks. Infinite (or very long living) callbacks, such as event handlers that fire multiple times. There are also single fire callbacks such as Promises. Promises are not as bad since they only resolve once before they can be cleaned up. However, they also live a long time. E.g. data requests can sometimes have very long timeouts to handle low-latency/bandwidth environments. The example above of a 30 second timeout is also an example of a long living promise. If your components frequently unmount and remount, you'll accumulate a lot of hanging promises and finally crash. Therefore, it really bothers me that the JS community has adopted Promises at scale since it doesn't come with a cleanup mechanism. However, from a pragmatic point of view, we should probably allow this pattern to work since Promises are so common and make it very difficult to do clean up. You've been warned. The big issue is that we can't really tell if the callback is coming from a Promise or a long living event. Failure to cleanup a long living event is a very common and dangerous mistake. So, how do we prevent that mistake from happening? Even if the current callstack is coming from a Promise, that's no guarantee. That Promise could've fired a persistent event subscription that called setState. E.g. a Flux store. Ultimately, I think that the solution is to build support for Promises in state. That way we can easily make an exception and ignore the warning if the Promise resolves late. We should also build in support for Observables that make it easy to get cleanup for free. We could easily ignore these setState calls but we'd need another way to ensure that you're not making common dangerous mistakes. |
@sebmarkbage thanks for these points. But I think that you position React as a police to enforcing good practices (in your subjective opinion) by blowing up an app with throwing hard errors. IMO warning would be good enough. Thank you for consideration. |
@shubik React already fails pretty softly in a bunch of places that I believe should be fatal. It's a complicated balance of making it abundantly clear to a developer exactly when/where things go wrong, vs. failing quietly. I agree with @sebmarkbage that we need a way to ensure developers are not making common dangerous mistakes, and provide escape hatches (like isMounted) when developers know what they're doing. That said, my intuition would be that setting state on an unmounted component would be analogous to a noop, so I tend to favor removing this error. |
@JSFB I completely agree that ideally you would ignore setState on unmounted component and (at a given log level) print a respective warning. I would expect a hard error when your library has an internal error situation, not when it's an expected (although not desirable in your opinion) use case. |
We'll probably make this a warning. However, note that we treat React warnings as if they're errors, except with a different code path in the error case. I.e. you should strive to never have any warnings in your code base. Even with a warning, it's not a fully supported use case and risk breaking. I would like to be able to fully support the use case, but what's the API? I don't know. How do we distinguish between the clearly bad case (hanging callback) and a valid (in your opinion) pattern? |
@sebmarkbage async XHR is a bad case but also the most common case, probably more common than timeouts or intervals that some of the commenters talked about - but at the other hand, as I and others pointed out, we are forced to check isMounted before every setState to make sure React never errors out. As I said, my main argument is that it makes more sense to throw an error if it's an internal error of React (e.g. using wrong arguments on React methods, etc.), not because of executing an async callback. In any case, thanks a lot for your time and for having a dialog with us about this. |
A common case for me is non-React (e.g. |
@gaearon that sounds like a bug either in your code or in React? Do you have a case that you're able to reproduce that we can take a look at? |
@sebmarkbage Yeah I'll see if I can extract it. |
I have been told by a reliable source that componentWillMount, render and componentDidMount happen together, so there is no chance of a callback returning somewhere in the middle and being confused by isMounted. So this issue now refers only to the setState when unmounted case. |
Don't want to pile on, but I think an additional issue (I can open a separate issue for it maybe?) is that isMounted is meant to be used as a guard against an unsafe setState, but during componentWillUnmount, isMounted is true but it is not safe to call setState. It would seem to me that this is an issue regardless of whether or not you should be calling setState in componentWillUnmount – whatever the guard is for setState, it should in that case return the status of "should not set state" during componentWillUnmount |
I have a case where we are using side-loading (Flux stores) at multiple levels within the component tree. There is a callback for the I feel like side-loading being a first class citizen in React will cause this kind of case to be more common. |
@mridgway Does the child component unsubscribe from the store on unmount? If so, it shouldn't get event callbacks after unmount unless I'm missing something. |
It does unsubscribe in unmount, but the |
File an issue in React Redux and maybe we can find a way to fix it. |
I've had this issue so many times. there are a bunch of solutions I've read up on too. either way, @donabrams gave a great response to @roman-cliqr SO issue - its the best answer to this problem i've seen so far. |
This has bitten me as well. If you have any actions triggered by non-react-events then your calls to
Checking |
I am experiencing this exact some problem (attaching handlers in DidMount, removing in willUnmount, still getting a setState warning). Are there any updates regarding this open issue? I even tried manually implementing an isMounted to verify, but I still end up with the same warning. Crazy thing is that the application still works fine, it just generates a ton of warnings. |
Yeah you have to call it something like |
Haha thank you - no I mean the warnings are about setState :) I did call it something like bComponentMounted to avoid confusion. |
+1 for setStateIfMounted for small harmless callbacks. |
Seems I was able to get around the isMounted/setState inconsistencies by just declaring a separate 'private' variable 'mounted' on the instance, and setting it from componentWillMount() and componentWillUnmount(), ie:
Seems to work as intended. If anyone knows why this might not be ideal, I'm all ears. |
This discussion is a little old but I think we have a reasonable conclusion here. If you can, we recommend that you use APIs that provide cancellation. For example, axios lets you create a componentDidMount() {
this.cancelSource = axios.CancelToken.source();
axios.get('/user/12345', {
cancelToken: this.cancelSource.token
}).then(
// ...
);
}
componentWillUnmount() {
this.cancelSource.cancel()
} I don’t agree cancellation is necessarily tedious—if it is, I suggest using a different library or asking the library you use to add a cancellation API. This is the right way to solve this problem because it also eliminates a whole class of bugs. For example, if you fetch data based on props, you should also cancel any requests for previous props after they change—or you risk getting the new response earlier than the older response, potentially displaying the wrong UI. Cancellation already solves these race conditions so this is one more reason to embrace it. If you can’t (or choose not to) implement cancellation for some reason, then setting a field is the way to go. Yes, this is clunky, but this clunkiness reminds you that:
In any case, React doesn’t throw errors when this happens anymore. But we will intentionally keep the suboptimal case clunky so that the developers have incentives to implement the right solution (cancellation). There are some edge cases related to libraries like Redux. I would just recommend to not perform side effects in So, to sum up:
I think this covers everything in this issue, so I’m closing it. |
Regarding the case in #2787 (comment), it seems like reordering |
Can you contextualize this part of your comment for me, please? "Your application is using network less efficiently" Cancelling a Promisified HTTP request which ultimately calls xhr.abort simply unregisters the callback on the client side. It does not close the socket in the browser since many requests or tabs could be using it at once (AFAIK sockets are expensive so they are likely to be shared) Cancellation on client side will not automatically interrupt a db query on the server (moreover, if it happens to be a mutative operation it won't roll back automatically) so in all cases other than browser closing the socket (user closes browser) the server will end up sending the request to the client anyway, thus using network bandwidth. I'd like to be corrected on this if I'm wrong on this or if there is more subtleness to your comment. Would be good to have greater context around this discussion that includes server and network. Thanks, |
That's fair, I didn't know |
redux-logic has a nice cancellation mechanism. |
@gaearon Thank you very much for summarising the solutions to this issue. Exactly what I was looking for. Just one question to the existing "setting a field" solution. Is this the recommended way to do this (e.g. set the mounted flag in |
I agree |
The native browser fetch API is promise based and cancellable through RxJS |
I wrote a blog article demonstrating how to do use AbortController in a simple React use case. I'd appreciate any feedback if I got something wrong. http://robwise.github.io/blog/cancel-whatwg-fetch-requests-in-react |
For the sake of this, I will simplify the workflow into three stages:
The particular case I'm worried about is when you trigger a promise (or similar delayed action) from componentWillMount:
The
.then()
could be called in two circumstances, synchronously in the same call stack, or asynchronously in a later stack (I know, mixing async and sync in the same signature is a bad idea, but for the sake of argument).In the asynchronous case: the promise above could resolve at any time in the future, so in order to prevent calling setState when we aren't allowed to, we add the
isMounted()
check. But what if the promise resolves while the component is still in the mounting state, outside of the componentWillMount stack?In the synchronous case: we will enter the block still in the mounting state, which means
isMounted()
will be false and my state won't be set.Depending on the answers to my async questions I can see two outcomes:
setState()
is safe to call or not, asisMounted()
is incomplete. The user has to manually track the state of the component and if it has been unmounted or not.Thoughts? Have I invented an issue where none exists?
The text was updated successfully, but these errors were encountered: