Clean Up code not works correctly in React when using AbortController

Viewed 49

I'm using a Custom useHttp hook to fetch some data from api, the problem is that I want to abort the fetching operation when the user navigates to another page and prevent weird errors. for this I'm using AbortController API inside my hook and whenever the component unmounts the function will be fired. but when my component renders, I will get an error that tells me the operation was aborted!

Hook Code :

import { useEffect , useState , useCallback , useRef } from 'react';

export const useHttpClient = () => {
   const [isLoading, setIsLoading] = useState(false);
   const [error, setError] = useState();


   const activeHttpRequests = useRef([]);

   const sendRequest = useCallback(async (URL, method = 'GET', body = null, headers = {}) => {
      setIsLoading(true);


      
      const httpAbortController = new AbortController();
      activeHttpRequests.current.push(httpAbortController);

      try {
         const res = await fetch(URL, {method, body, headers, signal: httpAbortController.signal});
         const resData = await res.json();
         
         activeHttpRequests.current = activeHttpRequests.current.filter(reqCtrl => reqCtrl !== httpAbortController)
         console.log(activeHttpRequests)
         
         if(!res.ok) {
            throw new Error(resData.error);
         }
         setIsLoading(false)   
         return resData;
      } catch (error) {
         setIsLoading(false)   
         setError(error.message)
         throw error;
      }
   }, [])

   const clearError = () => {
      setError(null);
   }


   useEffect(() => {
      return () => {
         activeHttpRequests.current.forEach(abortCtrl => abortCtrl.abort());
      };
   }, []);

   return {isLoading, error, clearError, sendRequest}
}

and this is the code inside my component :

// Modules
import React, { useEffect, useState } from 'react';

// Hooks 
import { useHttpClient } from '../../../helpers/hooks/http-hook';

// Components
import UserList from '../components/user-list/user-list.component';
import ErrorModal from '../../shared/error-modal/error-modal.component';
import Spinner from '../../shared/loading-spinner/spinner.component';


const Users = () => {
   const { isLoading , error , clearError , sendRequest } = useHttpClient();
   const [loadedUsers, setLoadedUsers] = useState();
   
   useEffect(() => {
      const fetchUsers = async () => {
         try {
            const URL = 'http://localhost:5000/api/users';
            const res = await sendRequest(URL);
            setLoadedUsers(res.users);
         } catch (error) {}
      }
      fetchUsers();
   }, [sendRequest])


   return (
      <React.Fragment>
         <ErrorModal error={error} onClear={clearError} />
         { isLoading && <Spinner overlay/> }
         { !isLoading && loadedUsers && <UserList users={loadedUsers} /> }
      </React.Fragment> 
   );
}
export default Users;
1 Answers

Anytime your component rerenders the clean up effects of useEffect from the previous render are run before running the effects in the next render. You call fetchUsers() when the component mounts, which sets, setLoadedUsers which would result in a rerender, which will run your clean up code that tries to cancel the nonexistent request.

Update your cleanup code to this, and it should work

  useEffect(() => {
      return () => {
        if (activeHttpRequests.current.length) {
          activeHttpRequests.current.forEach(abortCtrl => abortCtrl.abort());
        };
      };
   }, []);
Related