Stop handling async effects this way!
The #1 mistake people make and how to correct it
Handling async tasks is a necessity. In my opinion, React docs aren’t exactly elaborating about it as it should, at least in the context of effects. Let’s take the following code snippet as an example:
We’re fetching some records from an API and updating the component’s state, expecting them to eventually appear in the view, this way or another. We’re waiting for the promise to resolve, and in the meantime, there’re other tasks running in the background. Even though we wait for the promise, React doesn’t pause its execution. A couple of things might go wrong in the process, as a result:
- The component might unmount.
- The effect dependencies might change.
In either of these cases, the execution should stop, because it’s no longer relevant, and can cause memory leak. We’re running multiple concurrent events, unintentionally. This means that one execution can affect other, also known as race condition. Here’s an illustration of the scenario using a diagram:
In the diagram I’m basically trying to show you that even though the query was changed, we would see the results for the old query, not the new one. The first execution is clearly irrelevant. If so, what’re possible solutions to that?
There’s a couple. One uses React at its vanilla form, and the other requires a library. I’ll start with the first solution so you could better understand what’s going on, and what the library is trying to solve.
Using a couple of refs (
useRef), we can check if there was an input change, and if the component is still mounted:
This checkup should repeat itself after each and every promise resolution, to avoid race conditions and possible memory leaks. Needless to say that this process is tedious and time consuming, it requires us to overthink and write extra code. Long term, it gives us a lot of control over the execution but it’s extremely human error prone.
The alternative solution is a lot better for most uses cases, but it requires us to understand generators and use a third party library. Here’s how it looks like:
useAsyncEffect hook uses the generator function to determine whether the execution should proceed or not, given certain dependencies. The generator function has an equivalent behavior of
await, only it’s aware of the component’s state. Let’s look at the following diagram and try to better understand the process behind it:
useAsyncEffect hook gains control over the execution using an iterator. It will keep resolving promises and call
iterator.next(), unless a change was made in the dependencies, or the component was unmount. It will run the checkup for us, instead of us having to do it manually.
This is a lot better solution long term, because it requires a lot less maintenance. While this is so, we will have less control over the execution, which is not always what we need. A good app would use a mixture of both solutions!
yarn add react-async-core-hooks
The library also exports a couple of extra methods (for now) —
useAsyncCallback, which pretty much work on the same principals I mentioned in this article.
Feel free to ask questions and happy coding!