Enhancing React/NextJS app with native AbortController interface in JavaScript/TypeScript
Introduction
Have you ever encountered a situation while building an app using JavaScirpt, that you need to cancel or stop an HTTP request prematurely? This can be for several reasons, one would be to stop processing any fetched data that is old and is not needed anymore. imagine an input field, that would fire up search each time your User puts in new input, aside from the debouncing and waiting a bit until the user finishes putting in an input, the input field can make several requests until the user finishes writing in the main text that they want to be used, in that scenario all those search results or responses from the older requests will be coming to frontend and our frontend would get busy processing the data that we don’t need, and we get unlucky and the timing is not correct, then we can even have the risk of displaying the incorrect data, instead of the response for the most recent request. In this post, we'll talk about how using AbortController can help improve your app's performance by managing network requests more efficiently.
Understanding AbortController
AbortController lets you cancel a single ongoing web request whenever you need to. This is particularly useful in situations where you want to avoid unnecessary processing or make sure that the most recent and relevant data is being fetched, if a user is typing in a search box, you can abort the previous search request to prevent redundant processing. This would save up resources as it would prevent the app from processing irrelevant data and would probably also help with the app performance.
We should also consider the following point: Canceling a request on the frontend using AbortController doesn't necessarily guarantee cancellation on the backend. The backend may still process the request to some extent, depending on its structure. To fully cancel a request on the server side, we might have to take some additional steps. Of course, the Backend and also the DB connection should support this kind of cancellations to be able to achieve it in those areas as well. Some of the ways and strategies that we can achieve this are: using long-polling with timeouts or websockets with close events.
Implementing AbortController in the React/NextJS app
When it comes to using AbortControllers with a React app, useRefs are the way to go.
Refs are perfect for storing and initializing AbortControllers because they persist data across renders without causing re-renders. We can set up an AbortController in a useEffect hook and pass its signal to our fetch or axios call. If the component unmounts or the request is no longer needed, the AbortController can abort the request.
import { useEffect, useRef } from 'react';
const MyComponent = () => {
const abortControllerRef = useRef(new AbortController());
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('/api/data', { signal: abortControllerRef.current.signal });
// handle response
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request was aborted');
} else {
console.error('Fetch error:', error);
}
}
};
fetchData();
return () => {
abortControllerRef.current.abort();
};
}, []);
return <div>MyComponent</div>;
};
Using Abort Controller with React Hooks
We know some libraries give us access to some useful hooks, libraries like react-use
that can take us all the back to the era of the class components and give us the possibility to perform a task or execute a function when the component has been unmounted. in this case, a hook named useUnmount
can help us achieve something similar to the return function of useEffect.
import { useEffect, useRef } from 'react';
import { useUnmount } from 'react-use';
const MyComponent = () => {
const abortControllerRef = useRef(new AbortController());
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('/api/data', { signal: abortControllerRef.current.signal });
// handle response
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request was aborted');
} else {
console.error('Fetch error:', error);
}
}
};
fetchData();
}, []);
useUnmount(() => {
abortControllerRef.current.abort();
});
return <div>MyComponent</div>;
};
Using AbortController in User-Triggered Requests
Canceling ongoing requests is particularly useful in user-driven applications, like search functionalities or form submissions. For example, if a user is typing into a search field, you can cancel the previous search request as soon as a new character is typed. This makes sure the backend isn't overwhelmed with outdated requests, and the user gets the most relevant results promptly.
const handleSearch = (query) => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
abortControllerRef.current = new AbortController();
fetch(`/api/search?q=${query}`, { signal: abortControllerRef.current.signal })
.then(response => response.json())
.then(data => {
// handle data
})
.catch(error => {
if (error.name !== 'AbortError') {
console.error('Fetch error:', error);
}
});
};
Conclusion
By using the AbortController interface in our JavaScript/TypeScript app we can enhance our applications performance. Of course, we should still keep in mind that using AbortController doesn't mean that the Backend has also canceled the call and won't process it anymore. There should be a backend structure in place to support such an action on both the Frontend and also Backend side of our app.
Despite all the mentioned limitations of the AbortController, I still think by adding this interface when needed, we can improve our applications' reliability and predictability.