Race conditions in React and beyond. A race condition guard with TypeScript

JavaScript React TypeScript

Fetching resources is a bread and butter of the frontend development. When doing so, we might encounter a set of issues related to race conditions. In this article, we identify them and provide a solution.

Defining a race condition

The term “race condition” dates back to as far as 1954. We use it in the field of not only software but also electronics. It describes a situation when the behavior of our system is dependant on a sequence of events, but its order is uncontrollable. One of the apparent examples of race conditions is present in multithreading when two or more threads attempt to change shared data. Therefore, they are racing to access it.

Our code does not need to utilize multiple threads to have a race condition. A similar issue might occur with asynchronous code. Imagine the following situation:

  • The user visits the   page intending to view the post number 1.
    • we start fetching it, and it takes a while
  • Meanwhile, the user changes the page to 
    • we begin to fetch post number 2 that loads almost immediately
  • After the above completes, the first request finishes and the content of the page displays the post number 1

Unfortunately, once we create a Promise, we can’t cancel it. There has been quite a lot of discussion about whether or not it should be implemented. There even was a proposal for it, but it has been withdrawn.

If you want to know more about promises, check out Explaining promises and callbacks while implementing a sorting algorithm

Reproducing the issue in React

The above issue is quite easy to reproduce, unfortunately.

All of the below examples use React with TypeScript.
While making this article I use an API that I’ve built with Express. If you want to know more about it, check out this TypeScript Express series

The above component loads the post based on the URL of the application. The whole logic happens in the   hook.

If you want to know more about how to design custom hooks, check out JavaScript design patterns #3. The Facade pattern and applying it to React Hooks

Unfortunately, the above code is prone to race conditions. The most straightforward fix for it is to introduce an additional   variable, as Dan Abramov described in his article on overreacted.io.

By returning a function in our  hook, we can perform a cleanup. React runs this function when the component unmounts, as well as after every render. Thanks to that, it prevents setting the post from a previous request.

If you want to know more about how the useEffect hook works, check out Understanding the useEffect hook in React

Creating a race condition guard

The above solution might not work for you in a more complex situation. It also gets a bit messy, because we need to remember always to check the   variable. Instead, we can create a custom race condition guard. It’s a concept that I took from the article of Sebastien Lorber and built on top of it. Let’s walk through it step by step.

First, we globally keep a reference to a promise:

We update it every time we start making a request.

Then, we can use the  . Since we keep a reference to the last promise, we can check if the one that resolved matches that.

The above logic makes sure that we don’t update the state with data from an old promise. Unfortunately, we still need to check if the promises match in every callback.

A way to resolve the above issue is to return a promise that never resolves.

The above logic is still a bit messy. Let’s create a class that takes care of all of the above.

The   function returns a callback. It returns a promise that never resolves if the data is too old.

Moving all of this logic to a separate class cleans up our code. The only thing left to do is to use our  class.

The default value of the   in our class is any, but we can easily change that by using an interface:

This logic would be even cleaner if we would create a separate function that wraps the Fetch API with an additional logic. If you want to know more, check out TypeScript Generics. Discussing naming conventions

Summary

Being aware of the issue discussed in this article is an important thing. We should choose the fix taking a few things into consideration. The first solution that we’ve seen today is strictly limited to React and might be a fitting choice in many situations. The second approach works in many different cases and is completely separate from React. You can also use it regardless if you use TypeScript, or not. Also, you might want to look into solutions like Redux-Saga and RxJS.

avatar
  Subscribe  
newest oldest most voted
Notify of
Kacper Pietrzak

I’m curious how a big amount of Pending promises affects the event loop. Do they get cleaned up from memory after some time or do they stay there until the session ends? I assume that even tens of thousand of unresolved promises wouldn’t have significant performance on the app, but It would cool to know what exactly is happening.

Nux
Nux

Most probably they are removed as any other object. If you don’t save a reference to a Promise it should be removed.

Pavel
Pavel

Should be response?.ok

Krzysztof Hamerszmit
Krzysztof Hamerszmit

And what about the Abort controller? Isnt it a perfect use case for it? https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort

Lukas Wollmann
Lukas Wollmann

The safer approach would be to not do this inside components, but outside of the react lifecycles. Hooks may advocate doing data fetching and so on within components, but … don’t.