useEffect(), Http Requests & Aborting
Sending a Http request with useEffect() might be trickier than you think - or did you consider aborting requests and avoiding race conditions?
Making Http Requests with React useEffect - Correctly
Fetching data from an API is something that is essential part of any single page application and it is important to do it correctly to avoid running into bugs that are hard to detect.
It's easy to introduce subtle errors - like fetching data when it's no longer needed, causing infinite loops, hammering APIs etc.
In functional components, side effects like Http requests are made using the useEffect()
hook. Mostly, Http requests are made after component has mounted but sometimes you also may need to make Http request when either state or props of the component changed.
In this tutorial, we will make the request to jsonplaceholder API to fetch a single todo.
To understand this tutorial, you need to have a basic understanding of making requests with fetch()
, in addition, basic understanding of promises is assumed.
First of all, you need to import the useEffect
and useState
hooks in your component. The useEffect
hook will later contain the code that makes the request whereas the useState
hook will be used to save the fetched data in the component's state.
import React, { useEffect, useState } from 'react';
Now let's make a Http request to jsonplaceholder API:
const [todo, setTodo] = useState();useEffect(() => {fetch('https://jsonplaceholder.typicode.com/todos/1').then((response) => response.json()).then((data) => setTodo(data)).catch((error) => console.log(error.message));}, []);
The above code leads to a Http request being sent to the API when the component mounts. The request is only sent in that case since useEffect()
has an empty dependencies array ([]
).
You can learn more about useEffect()
in our React course or here.
Once the response was parsed and the data extracted, the state updating function provided by useState()
is used to set the loaded data
as a new todo
.
Preventing Memory Leaks
This code will make the request and fetch a single todo from the jsonplaceholder api and save it in the state of our component BUT this code has a memory leak which is not related to fetching data but related to setting the data in component's state.
Imagine a case where out component is mounted and useEffect()
hook executes but before the Http request completes, our component is unmounted from the DOM.
Although our component was unmounted, the response of our Http request will still be returned and parsed, the setTodo()
function will still be called. Therefore, the state of this (now unmounted component) will be updated - even though it's not mounted anymore.
Therefore, as soon as the setTodo()
function is called, React will throw a warning.
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
As you can tell from the warning message, our code tried to set state of an unmounted component.
We're not using componentWillUnmount
but we're definitely having the problem of updating state of a component that's not mounted anymore.
To fix this memory leak, we can take following steps:
Inside the
useEffect()
hook, we can declare a variable with an initial boolean value oftrue
.Before calling the
setTodo()
function to save the fetched data in the state, check if the value of the variable defined in first step is true or not. If it istrue
, call the state updating function - otherwise no action is needed.Inside the cleanup function of the
useEffect()
hook, set this variable's value tofalse
.
The code, after above mentioned steps, will look like this:
useEffect(() => {let isActive = true;fetch('https://jsonplaceholder.typicode.com/todos/1').then((response) => response.json()).then((data) => {if (isActive) {setTodo(data);}}).catch((error) => console.log(error.message));return () => {isActive = false;};}, []);
Since the cleanup function of useEffect()
is executed when the component unmounts, isActive
is set to false. This prevents setTodo()
from being called once the request completes.
We Had A Race Condition!
This simple fix will not only prevent our code from causing a memory leak, but it will also fix another problem: A race condition.
This problem occurs when our useEffect()
hook depends on any prop or state and makes a request to the API when any of the values, that our useEffect()
hook depends on, changed.
Here's an example
Consider a case where instead of always fetching a single todo with id 1, our component receives an id as a prop and we make a request to jsonplaceholder API to fetch a todo with that id passed as a prop.
In this case, we need to specify the id
prop as a dependency of the useEffect()
hook so that this hook runs whenever the id
prop changes
const { id } = props; // using destructuring to get just the IDuseEffect(() => {// other code}, [id]);
If we receive an id
prop equal to 1, the useEffect()
hook will execute and make the request to the API to fetch a single todo with id 1.
But as the request is in progress, the id
prop might change and its value is now 2.
Our useEffect()
hook will again execute and make another request to jsonplaceholder API, this time to fetch a single todo with id 2.
What will happen if, for some reason, this second request completes before the first request that was made with id 1?
In that case, our component state will contain the todo with id 2 but after that, when first request completes, the setTodo()
function will be called and our component's state will be updated with the todo for id 1.
That's definitely not what we want.
With the code used above, this problem will be prevented. Because when the id prop changes to 2, before making another request, the useEffect()
hook will first run the cleanup function and set the isActive
variable to false
.
Therefore, whenever the response of the first http request returns, our component's state won't be updated.
In other words, the first effect will be "cancelled" (kind of) when the cleanup function runs before running the useEffect()
hook again. So our component state will correctly update the todo
with the data for id 2 after the response for second http request was parsed.
Cancelling Fetch
The above written code with the isActive
variable does a great job of preventing the memory leak and avoiding the problem of a race condition, but there's an alternative that might be a bit more elegant.
Instead of preventing state updates by using a variable, we could cancel the Http request, made via fetch()
, by using a built-in JavaScript object called AbortController
(MDN docs).
Cancelling the request involves the following steps:
Create an instance of
AbortController
Get a reference to the
AbortSignal
object using thesignal
property of theAbortController
object that was created in step 1Pass this
AbortSignal
object as an option to thefetch()
functionInside the cleanup function of the
useEffect()
hook, call theabort()
function on the instance of theAbortController
created in step 1
We can change our code that uses the isActive
variable, to use AbortController
by implementing the above mentioned steps:
useEffect(() => {const abortCtrl = new AbortController();const opts = { signal: abortCtrl.signal };fetch('https://jsonplaceholder.typicode.com/todos/1', opts).then((response) => response.json()).then((data) => setTodo(data)).catch((error) => console.log(error.message));return () => abortCtrl.abort();}, []);
When any request is cancelled using the AbortController
object, an AbortError
is thrown. We can use that to handle this case with any code of our choice.
// ... promise chain.catch((error) => {if (error.name == 'AbortError') {console.log('request was cancelled');}});
Keep in mind that, at the time of writing this article, AbortController
is an experimental technology, meaning its browser support is not that great. At the moment, it's not supported by the IE browser at all. So when using AbortController
, make sure you take its browser support into consideration.