4

I have useEffect callback where I'm fetching some data from several API. To do that I'm using Promise and my code looks like that:

useEffect(() => {
   const fetchAllData = async () => {
      const resourceOne = new Promise((resolve) => {
         // fetching data from resource one and changing some states
      })
      const resourceTwo = new Promise((resolve) => {
         // fetching data from resource two and changing some states
      })
      const resourceThree = new Promise((resolve) => {
         // fetching data from resource three and changing some states
      })

      await Promise.all([resourceOne, resourceTwo, resourceThree])
      .then(() => {
         // changing some states
      })
   }

   return fetchAllData()
},[])

How I understand in this situation useEffect unmount before fetching all data and then gives me an 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 a useEffect cleanup function.

So how to write correctly cleanup code to avoid this warning?

Romanas
  • 547
  • 7
  • 25

2 Answers2

4

You can use a boolean and toggle it in the cleanup function (the function that is returned in the callback passed to useEffect)

useEffect(() => {
    let shouldUpdate = true;

    const fetchAllData = () => {
       const resourceOne = new Promise((resolve) => {
          // fetching data from resource one and changing some states
       })
       const resourceTwo = new Promise((resolve) => {
          // fetching data from resource two and changing some states
       })
       const resourceThree = new Promise((resolve) => {
          // fetching data from resource three and changing some states
       })
 
       Promise.all([resourceOne, resourceTwo, resourceThree])
        .then(() => {
           if(shouldUpdate) {
               // update state
           }
       })
    }
 
    fetchAllData()
    return () => {
        shouldUpdate = false;
    }
 },[])

If the component is unmounted the cleanup function will be called and shouldUpdate will change to false. When the promises resolve, the state will not update as shouldUpdate is no longer true.

Ramesh Reddy
  • 10,159
  • 3
  • 17
  • 32
2

You can make use of Abort Signal, to abort all promises,

const controller = new AbortController();

const fetch = new Promise((resolve, reject) => {
  // fetch data here
  controller.signal.addEventListener('abort', () => reject());
});

controller.abort(); // promise is cancelled

Translating to your use-case:

const controller = new AbortController();
useEffect(() => {
   const fetchAllData = async () => {
      const resourceOne = new Promise((resolve, reject) => {
         // fetching data from resource one and changing some states
          controller.signal.addEventListener('abort', () => reject());
      })
      const resourceTwo = new Promise((resolve, reject) => {
         // fetching data from resource two and changing some states
          controller.signal.addEventListener('abort', () => reject());
      })
      const resourceThree = new Promise((resolve, reject) => {
         // fetching data from resource three and changing some states
          controller.signal.addEventListener('abort', () => reject());
      })

      await Promise.all([resourceOne, resourceTwo, resourceThree])
      .then(() => {
         // changing some states
      })
   }

   fetchAllData();

   return () => {
        controller.abort(); // all promise are cancelled where 'abort' listener is specified
    }
},[])

And call controller.abort() to cancel one or more number of promises where you included the event listener when the component unmounts.

When abort() is called, the promise rejects with an AbortError, so you can listen to that and handle aborted rejects differently.

PsyGik
  • 3,535
  • 1
  • 27
  • 44