

Dennis Maina
Published on 20th October, 2022 (11 months ago) ● Updated on 20th October, 2022
React: AbortController Race Conditions
(5 minutes read)
Using AbortController to deal with Race Conditions in React
When developing applications, data fetching is one of the most fundamental tasks. Despite that, there are some things to watch out for: one of them is race conditions. This article explains what they are and provides a solution using the AbortController.
Identifying a race condition
A “race condition” is when our application depends on a sequence of events, but their order is uncontrollable. For example, this might occur with asynchronous code.
Here's an example using React and React router by defining the/posts/:postId
route and render the Post
component.
import React from 'react';
import { Routes, Route } from 'react-router-dom';
import Post from './Post';
function App() {
return (
} />
);
}
export default App;
In the Post
component, we either display a loading indicator or the fetched data. The most important part takes place in the usePostLoading
hook.
import { useParams } from 'react-router-dom';
import { useEffect, useState } from 'react';
interface Post {
id: number;
title: string;
body: string;
}
function usePostLoading() {
const { postId } = useParams<{ postId: string }>();
const [isLoading, setIsLoading] = useState(false);
const [post, setPost] = useState(null);
useEffect(() => {
setIsLoading(true);
fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`)
.then((response) => {
if (response.ok) {
return response.json();
}
return Promise.reject();
})
.then((fetchedPost: Post) => {
setPost(fetchedPost);
})
.finally(() => {
setIsLoading(false);
});
}, [postId]);
return {
post,
isLoading,
};
}
export default usePostLoading;
If you want to know more about the useEffect hook, check out
React: How to Make API Calls Using the useEffect Hook
Above, we fetch a post based on the URL. So, if the user visits /posts/1
, we send a GET request to https://jsonplaceholder.typicode.com/posts/1
.
Defining the race condition
The above approach is very common, but there is a catch. Let’s consider the following situation:
-
- The user opens
/posts/1
to see the first post,- we start fetching the post with id
1
, - there are some Internet connection issues,
- the post does not load yet,
- we start fetching the post with id
- Not waiting for the first post, the user changes the page to
/posts/2
- we start fetching the post with id
2
, - the post loads without issues and is available almost immediately,
- we start fetching the post with id
- The first post finishes loading,
- the
setPost(fetchedPost)
line executes, overwriting the current state with the first post, - even though the user switched the page to
/posts/2
, the first post is still visible.
- the
- The user opens
Unfortunately, we can’t cancel a promise once we create it. There was a proposal to implement a feature like that, but it has been withdrawn.
The most straightforward fix for the above issue is introducing a didCancel
variable, as suggested by Dan Abramov. When doing that, we need to use the fact that we can clean up after our useEffect
hook. React will call it when our component unmounts if useEffect
returns a function.
useEffect(() => {
let didCancel = false;
setIsLoading(true);
fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`)
.then((response) => {
if (response.ok) {
return response.json();
}
return Promise.reject();
})
.then((fetchedPost: Post) => {
if (!didCancel) {
setPost(fetchedPost);
}
})
.finally(() => {
setIsLoading(false);
});
return () => {
didCancel = true;
}
}, [postId]);
Now, the bug we’ve seen before is no longer appearing:
-
- The user opens
/posts/1
to see the first post,- we start fetching the post with id
1
,
- we start fetching the post with id
- Not waiting for the first post, the user changes the page to
/posts/2
,- the useEffect cleans after the previous post and sets didCancel to true,
- we start fetching the post with id
2
, - the post loads without issues and is available almost immediately,
- The first post finishes loading,
- the
setPost(fetchedPost)
line does not execute because of thedidCancel
variable.
- the
- The user opens
Now, if the user changes the route from /posts/1
to /posts/2
, we set didCancel
to true
. Thanks to that, if the promise resolves when we no longer need it, the setPost(fetchedPost)
is not called.
Introducing AbortController
While the above solution fixes the problem, it is not optimal. The browser still waits for the HTTP request to finish but ignores its result. To improve this, we can use the AbortController.
With it, we can abort one or more fetch requests. To do this, we need to create an instance of the AbortController
and use it when making the fetch request.
useEffect(() => {
const abortController = new AbortController();
setIsLoading(true);
fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`, {
signal: abortController.signal,
})
.then((response) => {
if (response.ok) {
return response.json();
}
return Promise.reject();
})
.then((fetchedPost: Post) => {
setPost(fetchedPost);
})
.finally(() => {
setIsLoading(false);
});
return () => {
abortController.abort();
};
}, [postId]);
Above, we pass the abortController.signal
through the fetch options. Thanks to that, the browser can stop the request when we call abortController.abort()
.
We can pass the same
abortController.signal
to multiple fetch requests. If we do that,abortController.abort()
aborts multiple requests.
The above also fixes the issue we’ve had with the race conditions.
-
- The user opens
/posts/1
to see the first post,- we start fetching the post with id
1
,
- we start fetching the post with id
- Not waiting for the first post, the user changes the page to
/posts/2
,- the
useEffect
cleans after the previous post and runsabortController.abort()
, - we start fetching the post with id
2
, - the post loads without issues and is available almost immediately,
- the
- The first post never finishes loading because we’ve already aborted the first request.
- The user opens
We can observe the above behavior in the network tab in the developer tools.
A crucial thing about calling the abortController.abort()
is that it causes the fetch promise to be rejected. It might result in an uncaught error.
To avoid the above message, let’s catch the error.
useEffect(() => {
const abortController = new AbortController();
setIsLoading(true);
fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`, {
signal: abortController.signal,
})
.then((response) => {
if (response.ok) {
return response.json();
}
return Promise.reject();
})
.then((fetchedPost: Post) => {
setPost(fetchedPost);
})
.catch(() => {
if (abortController.signal.aborted) {
console.log('The user aborted the request');
} else {
console.error('The request failed');
}
})
.finally(() => {
setIsLoading(false);
});
return () => {
abortController.abort();
};
}, [postId]);
Comments (0)

Domain Name Registration & Hosting
HostPinnacle Kenya is the best and cheapest web hosting company in Kenya with world-class web hosting packages and affordable web design offers. Apart from that we offer free life-time SSL certificate, affordable domain registration in Kenya and free whois privacy. We have an award-winning support team available 24/7/365 to help you with your queries.