What React hook must know: the cleanup of useEffect

Posted by direland on Wed, 01 Dec 2021 12:20:43 +0100

cleanup of React useEffect

  • It can avoid problems such as memory leakage in applications
  • Can optimize application performance

Before starting this article, you should have a basic understanding of what useEffect is, including using it to obtain data.

This article will explain the cleanup of useEffectHook, and I hope you can use it comfortably by the end of this article.

What is useEffectcleanup?

As the name suggests, useEffectcleanup is a function in Hook that allows us to clean up code before uninstalling components.

The useEffect hook can return a function.

cleanup prevents memory leaks and removes unnecessary and unwanted behavior.

useEffect(() => {
        effect
        return () => {
            cleanup
        }
    }, [input])

Why is useEffectcleanup useful?

As mentioned earlier, useEffectcleanup helps developers clean up, prevent unwanted behavior, and optimize application performance.

However, it should be noted that useEffectcleanup not only runs when the component wants to uninstall, but also terminates side effects after some properties and states change.

Let's look at this scenario: let's say that we pass a specific user id   fetch data , before the acquisition is complete, we change our mind and try to acquire another user. At this point, the id is updated and the previous fetch request is still in progress.

use Cleanup function If the request is aborted, the application will not leak memory.

When should useEffect use cleanup?

Suppose we have a React component to retrieve and render data. If a component is uninstalled before cleanup, useEffect attempts to update the status (on a component that is not installed) and gets the following error:

To fix this error, we used the cleanup function to resolve it.

According to the official document of React, "React performs cleanup when the component is unloaded. However... Side effects run at each rendering, not just once. That's why React also cleans up side effects in the previous rendering before the next side effect execution."

Cleanup is typically used to cancel all subscriptions and get requests.

Clean up subscriptions

To start cleaning up the subscription, we must first cancel the subscription because we don't want the application to leak memory. We want to optimize the application.

Cancel the subscription before uninstalling the component, set the variable isApiSubscribed to true, and then we can set it to false during uninstallation:

useEffect(() => {
    // set our variable to true
    let isApiSubscribed = true;
    axios.get(API).then((response) => {
        if (isApiSubscribed) {
            // handle success
        }
    });
    return () => {
        // cancel the subscription
        isApiSubscribed = false;
    };
}, []);

In the above code, we set the variable isApiSubscribed to true and then use it as a condition to process our successful request. However, we set the variable isApiSubscribed to false when we uninstall the component.

Cancel request

Method called to cancel fetch request: AbortController   Axios cancel token.

To use AbortController, we must use Constructor Create a controller. Then, when the fetch request starts, we pass it as an option to the requested object. AbortController() AbortSignaloption

This associates the controller and signal with the acquisition request and cancels it with the following command: AbortController.abort()  

>useEffect(() => {
    const controller = new AbortController();
    const signal = controller.signal;

        fetch(API, {
            signal: signal
        })
        .then((response) => response.json())
        .then((response) => {
            // handle success
        });
    return () => {
        // cancel the request before component unmounts
        controller.abort();
    };
}, []);

We can go further and add an error condition to the catch so that an error will not be thrown when the fetch request is aborted. The reason is that when uninstalling, we still try to update the status when handling errors.

All we can do is write a condition to know what kind of mistakes we will get;

If we receive Abort error , then we do not update the status:

useEffect(() => {
  const controller = new AbortController();
  const signal = controller.signal;

   fetch(API, {
      signal: signal
    })
    .then((response) => response.json())
    .then((response) => {
      // handle success
      console.log(response);
    })
    .catch((err) => {
      if (err.name === 'AbortError') {
        console.log('successfully aborted');
      } else {
        // handle error
      }
    });
  return () => {
    // cancel the request before component unmounts
    controller.abort();
  };
}, []);

Even if we navigate to another page before the request is completed, we will not receive the error again because the request will be aborted before the component is unloaded. If we receive an abort error, the status will not be updated.

So, how to use the Axios cancel option and the Axios cancel token to do the same thing,

We first store the data from Axios in a constant named source, pass the token as the Axios option, and then cancel the request with the following command: CancelToken.source()source.cancel()

useEffect(() => {
  const CancelToken = axios.CancelToken;
  const source = CancelToken.source();
  axios
    .get(API, {
      cancelToken: source.token
    })
    .catch((err) => {
      if (axios.isCancel(err)) {
        console.log('successfully aborted');
      } else {
        // handle error
      }
    });
  return () => {
    // cancel the request before component unmounts
    source.cancel();
  };
}, []);

As we did with AbortErrorin, AbortController and Axios gave us a called method isCancel, which allows us to check the cause of the error and know how to deal with it.

If the request to abort or cancel the Axios source fails, we don't want to update the status.

How to use useEffectcleanup

Look at an example of when the above error occurs and how to use cleanup when it occurs.

Create two files: Post and App

// Post component

import React, { useState, useEffect } from "react";
export default function Post() {
  const [posts, setPosts] = useState([]);
  const [error, setError] = useState(null);
  useEffect(() => {
    const controller = new AbortController();
    const signal = controller.signal;
    fetch("https://jsonplaceholder.typicode.com/posts", { signal: signal })
      .then((res) => res.json())
      .then((res) => setPosts(res))
      .catch((err) => setError(err));
  }, []);
  return (
    <div>
      {!error ? (
        posts.map((post) => (
          <ul key={post.id}>
            <li>{post.title}</li>
          </ul>
        ))
      ) : (
        <p>{error}</p>
      )}
    </div>
  );
}

This is a simple post component that gets the post and handles the get error every time it renders.

We import the post component in the main component, and click the button to display the post. Click the button again to hide the post, that is, mount and uninstall the post component:

// App component

import React, { useState } from "react";
import Post from "./Post";
const App = () => {
  const [show, setShow] = useState(false);
  const showPost = () => {
    // toggles posts onclick of button
    setShow(!show);
  };
  return (
    <div>
      <button onClick={showPost}>Show Posts</button>
      {show && <Post />}
    </div>
  );
};
export default App;

Click this button before Post rendering. Click this button again, and we will receive an error message in the console.

This is because ReactuseEffect is still running and trying to get the API in the background. After getting the API, it attempts to update the status, but this time on a component that is not installed, so this error is raised:

To clear this error and prevent memory leaks, we must use the above solution to implement cleanup. In this article, we will use AbortController:

// Post component

import React, { useState, useEffect } from "react";
export default function Post() {
  const [posts, setPosts] = useState([]);
  const [error, setError] = useState(null);
  useEffect(() => {
    const controller = new AbortController();
    const signal = controller.signal;
    fetch("https://jsonplaceholder.typicode.com/posts", { signal: signal })
      .then((res) => res.json())
      .then((res) => setPosts(res))
      .catch((err) => {
        setError(err);
      });
    return () => controller.abort();
  }, []);
  return (
    <div>
      {!error ? (
        posts.map((post) => (
          <ul key={post.id}>
            <li>{post.title}</li>
          </ul>
        ))
      ) : (
        <p>{error}</p>
      )}
    </div>
  );
}

We can see in the console that even after the request is aborted in the cleanup function, the uninstall will cause an error.

As we discussed earlier, this error occurs when we abort the fetch call.

useEffect catches the fetch error in the catch block, then attempts to update the error status, and then throws an error. To stop this update, we can use the condition and check the error type we get. if   else

If we abort an error, we do not need to update the status, otherwise we handle the error:

// Post component

import React, { useState, useEffect } from "react";

export default function Post() {
  const [posts, setPosts] = useState([]);
  const [error, setError] = useState(null);
  useEffect(() => {
    const controller = new AbortController();
    const signal = controller.signal;

      fetch("https://jsonplaceholder.typicode.com/posts", { signal: signal })
      .then((res) => res.json())
      .then((res) => setPosts(res))
      .catch((err) => {
        if (err.name === "AbortError") {
          console.log("successfully aborted");
        } else {
          setError(err);
        }
      });
    return () => controller.abort();
  }, []);
  return (
    <div>
      {!error ? (
        posts.map((post) => (
          <ul key={post.id}>
            <li>{post.title}</li>
          </ul>
        ))
      ) : (
        <p>{error}</p>
      )}
    </div>
  );
}

Note that this is only called when fetch is used

When using Axios, you should judge: err.name === "AbortError" axios.isCancel()

Done!

conclusion

How to use useEffectHook Clean up is very important to prevent memory leaks and optimize applications.

I hope this article is helpful to you.

Topics: React React Native