bg-img

    Created by Yousaf Khan
    Last Updated on July 09, 2020

    # 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.

    You can get all those JavaScript basics on MDN or in our “JavaScript - The Complete Guide” course.

    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:

    1. Inside the useEffect() hook, we can declare a variable with an initial boolean value of true.
    2. 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 is true, call the state updating function - otherwise no action is needed.
    3. Inside the cleanup function of the useEffect() hook, set this variable’s value to false.

    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 ID
    
    useEffect(() => {
      // 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:

    1. Create an instance of AbortController
    2. Get a reference to the AbortSignal object using the signal property of the AbortController object that was created in step 1
    3. Pass this AbortSignal object as an option to the fetch() function
    4. Inside the cleanup function of the useEffect() hook, call the abort() function on the instance of the AbortController 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.