question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Discussion: Async cleanups of useEffect

See original GitHub issue

Hi 👋

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.

  1. 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.
  2. 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.

cc @gaearon @bvaughn

Issue Analytics

  • State:open
  • Created 3 years ago
  • Reactions:5
  • Comments:15 (10 by maintainers)

github_iconTop GitHub Comments

2reactions
davidje13commented, Sep 14, 2020

I have a similar concern about this with maybe a more concrete example;

const MyComponent = ({ destination }) => {
  const handleMessage = useCallback((e) => {
    fetch(destination); // or some other state changing action, such as mutating a reducer in a context
  }, [destination]);

  useEffect(() => {
    window.addEventListener('message', handleMessage);
    return () => window.removeEventListener('message', handleMessage);
  }, [handleMessage]);

  return (<div>Listening for messages</div>);
}
  1. is the teardown still guaranteed to be called before the next setup? (e.g. if destination changes, is there any chance that both the old and new listener could be bound at the same time?)
  2. if I have a parent component like below, could 2 listeners be active at once during the layout transition?
const MyParentComponent = () => {
  const narrowView = useIsWindowNarrow();

  if (narrowView) {
    return (<section><MyComponent /></section>);
  } else {
    return (<section><div><MyComponent /></div></section>); // (i.e. something which prevents reusing the existing MyComponent)
  }
}

Should effects which use addEventListener be using useLayoutEffect? If so that should definitely be documented! or will this still be safe?

1reaction
gaearoncommented, Aug 23, 2020

By pushing a notification I was referring to a somehow orchestrated notification system that will display for several seconds a snackbar (with interactive user actions) or whatever on the screen.

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.

What bothers me about this, is that single frame is a place where side effects can be triggered. And a side effect, can do any sort of things (sockets, navigation, global mutations…). Which means for me that this is a place for bugs that are the hardest to find/debug.

This is still too vague to respond to directly. Like I noted above, we need to discuss a specific case.

Also, Let’s say I have a unit test which is frame-exact in which I unmount a component and I expect a function not to be called. If I understand well the react 17 behavior, some of my tests may fail depending on the machine I am running my tests on.

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.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Cleaning up Async Functions in React's useEffect Hook ...
To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function. The instruction is pretty clear and straightforward, " ...
Read more >
How to use async functions in useEffect (with examples)
The issue here is that the first argument of useEffect is supposed to be a function that returns either nothing ( undefined )...
Read more >
React Hook Warnings for async function in useEffect
18 Answers 18 · A package to make this easier has been made. · but eslint won't tolerate with that · there is...
Read more >
Successfully using async functions in React useEffect
How to avoid the exhaustive deps ESLint error by properly using JavaScript async functions within the React useEffect Hook.
Read more >
Cleanup Functions in React's UseEffect Hook — Explained ...
Cleanup functions in React's useEffect hook allow us to stop side effects that no longer need to be executed in the component.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found