Discussion: Async cleanups of useEffect
See original GitHub issueHi 👋
I’ve been asking about this on Twitter but was told that the issues here might be better to discuss this stuff.
My general concern is that async cleanups might lead to weird race conditions. It may be unwarranted but the concept itself sounds quite alarming to me and I’d like to discuss this, if possible.
If you go with the async cleanups then there is no guarantee that a scheduled work (or just any listeners) would get cleaned up before you get rid of a component instance, so for example:
useEffect(() => {
if (state !== 'foo') return
const id = setTimeout(() => setShouldAnimate(true), 300)
return () => clearTimeout(id)
}, [state])
This might not work as intended. There is an off-chance that the scheduled timeout will fire after the containing component unmounts but before the timer gets disposed.
Calling setState
after unmounting was always a sign of broken assumptions in the code or some programming error and React has been warning about it. I was told though that this has been accounted for and the warning is being suppressed now - so it won’t pop up for users if setState
got called in that short timeframe. So at least that’s OK.
I’m worried though that a disposed component can still cause an unwanted side-effect in a parent. One can imagine some scenarios where that would matter.
- orchestrating animation - an unmounted component tells the parent to trigger some sort of animation. The reason why the animation should happen is owned by a child, but it’s also based on an additional timer because the reason might become invalid if the user performs some invalidating action quickly enough. It’s not obvious here that
useLayoutEffect
should be used here to achieve instant cleanup. - similar case: orchestrating some in-product tour, triggering tooltips, arrows, whatever in the parent. It becomes even less apparent that this should be
useLayoutEffect
-based to achieve instant clean up as this is not related to layout, even remotely. This is business logic.
I hope my concerns are not warranted and you could clear up them for me, but right now I’m worried a lot that this is such a small difference for most of the people and that’s it’s hard to spot in the code that this might become a source of many very subtle bugs.
Issue Analytics
- State:
- Created 3 years ago
- Reactions:5
- Comments:15 (10 by maintainers)
I have a similar concern about this with maybe a more concrete example;
destination
changes, is there any chance that both the old and new listener could be bound at the same time?)Should effects which use
addEventListener
be usinguseLayoutEffect
? If so that should definitely be documented! or will this still be safe?Right, in which case it doesn’t strictly matter if it gets cancelled at the last moment or not. I think my argument above shows that, but let me know if I missed something.
This is still too vague to respond to directly. Like I noted above, we need to discuss a specific case.
I don’t see how that would be the case. The behavior is deterministic — the effects are flushed after the component is unmounted. To wait for both to happen in tests, you should use
act()
, just like a warning tells if you (if you forget to do it).Again, this fully mirrors the existing behavior of
useEffect
for mounting and updates.