Home > Mobile >  Best practices for async await when retrieving data from firestore
Best practices for async await when retrieving data from firestore

Time:02-19

I had this error before

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.

This is my useEffect where I retrieve data from firestore and it does remove the error:

 useEffect(async () => {
    let isMounted = true;
    const querySnapshot = await getDocs(collection(db, "product"));
    const arr = [];
    querySnapshot.forEach((doc) => {
      arr.push({
        ...doc.data(),
        id: doc.id,
      });
      if (isMounted) setProduct(arr);
    });
    return () => {
      isMounted = false;
    };
  }, []);

Is the useEffect alright or will this cause any problems in the future? And am I mounting this right?

CodePudding user response:

Ths look okay. But it's best to extract the fetching logic into a separate function instead of having the entire logic in the body of the useEffect. It gets easy to re-use the logic for functionality like retrying if the request failed on mount.

I would do something like this...

  • Note: I removed the isMounted as the effect with be run when the component is mounted.
  • Wrap the function in an async function(named or anonymous) inside the useEffect and run it immediately.
// Re-usable logic for stuff like reload/retry
const fetchProduct = async () => {
  const querySnapshot = await getDocs(collection(db, "product"));
  const arr = [];
  querySnapshot.forEach((doc) => {
    arr.push({
      ...doc.data(),
      id: doc.id,
    });
  });
  setProduct(arr); // Moved this out of the loop to be run once.
}

useEffect(async () => {
  (async () => {
    await fetchProduct();
    // await other async operations here.
  })(); // This immediately runs the func async.
}, []);

CodePudding user response:

Looking at your current code, you call useEffect with an async function that uses await. Because you are using an async function, the unsubscriber function you are returning to useEffect is wrapped in a Promise instead of being the raw callback itself. This means that the useEffect never sees that the return value is a callback function in time for it to be useful.

The solution to this is to make sure the function passed into the useEffect is not async.

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

  const doFetch = async () => {
    const querySnapshot = await getDocs(collection(db, "product"));
    const arr = [];
    querySnapshot.forEach((doc) => {
      arr.push({
        ...doc.data(),
        id: doc.id,
      });
      if (isMounted) setProduct(arr);
    });
  };

  doFetch() // start the async work
    .catch((err) => {
      if (!isMounted) return; // unmounted, ignore.
      // TODO: Handle errors in your component
      console.error("failed to fetch data", err);
    });

  return () => {
    isMounted = false;
  };
}, []);

There are useAsyncEffect implementations that allow you to skip the setState() and useEffect() boilerplate that you are currently using. I have detailed these in similar answers here and here.

  • Related