In your makeCancelable function you are just checking the value of hasCanceled_ after the promise has finished (meaning getData has already executed entirely):

const makeCancelable = (promise) => {
  let hasCanceled_ = false;

  const wrappedPromise = new Promise((resolve, reject) => {
    // AFTER PROMISE RESOLVES (see following '.then()'!), check if the 
    // react element has unmount (meaning the cancel function was called). 
    // If so, just reject it
    promise.then(
      val => hasCanceled_ ? reject({isCanceled: true}) : resolve(val),
      error => hasCanceled_ ? reject({isCanceled: true}) : reject(error)
    );
  });

  return {
    promise: wrappedPromise,
    cancel() {
      hasCanceled_ = true;
    },
  };
};

Instead, in this case I would recomend you to go for a simpler and more classic solution and use a isMounted variable to create the logic you want:

useEffect(() => {
  let isMounted = true
  const getData = async () => {
    const collectionRef_1 = await firestore.collection(...)
    const collectionRef_2 = await firestore.collection(...)
    if (collectionRef_1.exists && isMounted) {
      // this should not run if not mounted
    }
    if (collectionRef_2.exists && isMounted) {
      // this should not run if not mounted
    }
  }
  getData().then(() => setDataLoaded(true))
  return () => {
    isMounted = false
  }
}, [dataLoaded, firestore])
Answer from Rashomon on Stack Overflow
🌐
Dmitri Pavlutin
dmitripavlutin.com › react-cleanup-async-effects
How to Cleanup Async Effects in React
May 25, 2021 - Let's wire the above ideas and fix the <Employees> component to correctly handle the cleanup of the fetch async effect: ... Open the demo. let controller = new AbortController() creates an instance of the abort controller. Then await fetch(..., { signal: controller.signal }) connects the controller with the fetch request. Finally, the useEffect() callback returns a cleanup function () => controller?.abort() that aborts the request in case if the component umounts.
🌐
Stack Overflow
stackoverflow.com › questions › 75725559 › have-async-function-in-useeffect-return-the-cleanup-function-for-the-useeffect-i
reactjs - Have async function in useEffect return the cleanup function for the useEffect itself - Stack Overflow
I also can't move the async function inside of useEffect because this function is used by multiple components. :x ... You can't return the cleanup function if it's defined inside the asynchronous context, because the cleanup function needs to be returned synchronously.
Discussions

Discussion: Async cleanups of useEffect
Hi 👋 I've been asking about this on Twitter but was told that the issues here might be better to discuss this stuff. My general concern is that async cleanups might lead to weird race condition... More on github.com
🌐 github.com
15
August 21, 2020
reactjs - Clean up async function in an useEffect React hook - Stack Overflow
8 React useEffect hook and Async/await own fetch data func? 4 React useEffect cleanup function depends on async await result More on stackoverflow.com
🌐 stackoverflow.com
How to clean up a fetch request inside a useEffect if the async function is declared outside the useEffect block?
You can pass the ignore prop as an argument to your function, and the rest will be the same as in the tutorial. More on reddit.com
🌐 r/reactjs
6
3
March 20, 2023
React Hook Warnings for async function in useEffect: useEffect function must return a cleanup function or nothing
The reason React doesn’t automatically allow async functions in useEffect is that in a huge portion of cases, there is some cleanup necessary. The function useAsyncEffect as you’ve written it could easily mislead someone into thinking if they return a cleanup function from their async effect ... More on stackoverflow.com
🌐 stackoverflow.com
🌐
React
react.dev › reference › react › useEffect
useEffect – React
You can also rewrite using the async / await syntax, but you still need to provide a cleanup function: ... import { useState, useEffect } from 'react'; import { fetchBio } from './api.js'; export default function Page() { const [person, setPerson] = useState('Alice'); const [bio, setBio] = useState(null); useEffect(() => { async function startFetching() { setBio(null); const result = await fetchBio(person); if (!ignore) { setBio(result); } } let ignore = false; startFetching(); return () => { ignore = true; } }, [person]); return ( <> <select value={person} onChange={e => { setPerson(e.target.value); }}> <option value="Alice">Alice</option> <option value="Bob">Bob</option> <option value="Taylor">Taylor</option> </select> <hr /> <p><i>{bio ??
🌐
DEV Community
dev.to › elijahtrillionz › cleaning-up-async-functions-in-reacts-useeffect-hook-unsubscribing-3dkk
Cleaning up Async Functions in React's useEffect Hook (Unsubscribing) - DEV Community
December 2, 2021 - The instruction is pretty clear and straightforward, "cancel all subscriptions and asynchronous tasks in a useEffect cleanup function". Alright, I hear you React!
🌐
GitHub
github.com › facebook › react › issues › 19671
Discussion: Async cleanups of useEffect · Issue #19671 · facebook/react
August 21, 2020 - If you go with the async cleanups then there is no guarantee that a scheduled work (or just any listeners) would get cleaned up before you get rid of a component instance, so for example: useEffect(() => { if (state !== 'foo') return const id = setTimeout(() => setShouldAnimate(true), 300) return () => clearTimeout(id) }, [state]) This might not work as intended.
Author   Andarist
🌐
LogRocket
blog.logrocket.com › home › understanding react’s useeffect cleanup function
Understanding React’s useEffect cleanup function - LogRocket Blog
December 16, 2024 - Now that we understand how to make useEffect run once, let’s get back to our cleanup function conversation. The cleanup function is commonly used to cancel all active subscriptions and async requests.
🌐
Bitstack
blog.bitsrc.io › everything-you-need-to-know-about-useeffects-clean-up-function-in-react-dfa6bc75f4f3
Cleanup Function in useEffect() Hook: All You Need to Know | Bits and Pieces
October 1, 2023 - By using the cleanup function to cancel an active request, you can ensure that your application behaves correctly, even when requests return out of order. import { useState, useEffect } from "react"; function App() { const [data, setData] = ...
Find elsewhere
🌐
Medium
medium.com › @vishalkalia.er › what-is-the-useeffect-cleanup-function-and-how-it-works-83d8c67a1a10
What is the React useEffect cleanup function, and how it works? | by Vishal Kalia | Medium
December 20, 2022 - The useEffect cleanup function can be crucial when working with async operations, such as API requests because it allows you to cancel any ongoing async tasks before the component is unmounted.
Top answer
1 of 16
863

Note: If you want to support SSR/SSG for SEO, use framework specific api from React-router/Remix, Next.js or Gatsby.

For React version >=19

We now have nice loader and form action api:

Loading data:

function Comments({ dataPromise }) {
  const data = use(dataPromise);
  return <div>{JSON.stringify(data)}</div>;
}

async function loadData() {
  return { data: "some data" };
}

export function Index() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Comments dataPromise={loadData()} />
    </Suspense>
  );
}

Form actions

async function updateDataAndLoadNew() {
  // update data
  // load new data

  // return new data
  return { data: "some data" };
}

export default function Index() {
  const [state, action, isPending] = useActionState(
    async (prev, formData) => {
      return updateUserAndLoadNewData();
    },
    { data: "initial state, no data" } // can be null
  );

  if (state.error) {
    return `Error ${state.error.message}`;
  }

  return (
    <form action={action}>
      <input type="text" name="name" />
      <button type="submit" disabled={isPending}>
        Do it
      </button>
      {state.data && <p>Data {state.data}</p>}
    </form>
  );
}

For React version >=18

Starting with React 18 you can also use Suspense, but it's not yet recommended if you are not using frameworks that correctly implement it:

In React 18, you can start using Suspense for data fetching in opinionated frameworks like Relay, Next.js, Hydrogen, or Remix. Ad hoc data fetching with Suspense is technically possible, but still not recommended as a general strategy.

If not part of the framework, you can try some libs that implement it like swr.


Oversimplified example of how suspense works. You need to throw a promise for Suspense to catch it, show fallback component first and render Main component when promise it's resolved.

let fullfilled = false;
let promise;

const fetchData = () => {
  if (!fullfilled) {
    if (!promise) {
      promise = new Promise(async (resolve) => {
        const res = await fetch('api/data')
        const data = await res.json()

        fullfilled = true
        resolve(data)
      });
    }

    throw promise
  }
};

const Main = () => {
  fetchData();
  return <div>Loaded</div>;
};

const App = () => (
  <Suspense fallback={"Loading..."}>
    <Main />
  </Suspense>
);

For React version <=17

I suggest to look at Dan Abramov (one of the React core maintainers) answer here:

I think you're making it more complicated than it needs to be.

function Example() {
  const [data, dataSet] = useState<any>(null)

  useEffect(() => {
    async function fetchMyAPI() {
      let response = await fetch('api/data')
      response = await response.json()
      dataSet(response)
    }

    fetchMyAPI()
  }, [])

  return <div>{JSON.stringify(data)}</div>
}

Longer term we'll discourage this pattern because it encourages race conditions. Such as — anything could happen between your call starts and ends, and you could have gotten new props. Instead, we'll recommend Suspense for data fetching which will look more like

const response = MyAPIResource.read();

and no effects. But in the meantime you can move the async stuff to a separate function and call it.

You can read more about experimental suspense here.


If you want to use functions outside with eslint.

 function OutsideUsageExample({ userId }) {
  const [data, dataSet] = useState<any>(null)

  const fetchMyAPI = useCallback(async () => {
    let response = await fetch('api/data/' + userId)
    response = await response.json()
    dataSet(response)
  }, [userId]) // if userId changes, useEffect will run again

  useEffect(() => {
    fetchMyAPI()
  }, [fetchMyAPI])

  return (
    <div>
      <div>data: {JSON.stringify(data)}</div>
      <div>
        <button onClick={fetchMyAPI}>manual fetch</button>
      </div>
    </div>
  )
}
2 of 16
156

When you use an async function like

async () => {
    try {
        const response = await fetch(`https://www.reddit.com/r/${subreddit}.json`);
        const json = await response.json();
        setPosts(json.data.children.map(it => it.data));
    } catch (e) {
        console.error(e);
    }
}

it returns a promise and useEffect doesn't expect the callback function to return Promise, rather it expects that nothing is returned or a function is returned.

As a workaround for the warning you can use a self invoking async function.

useEffect(() => {
    (async function() {
        try {
            const response = await fetch(
                `https://www.reddit.com/r/${subreddit}.json`
            );
            const json = await response.json();
            setPosts(json.data.children.map(it => it.data));
        } catch (e) {
            console.error(e);
        }
    })();
}, []);

or to make it more cleaner you could define a function and then call it

useEffect(() => {
    async function fetchData() {
        try {
            const response = await fetch(
                `https://www.reddit.com/r/${subreddit}.json`
            );
            const json = await response.json();
            setPosts(json.data.children.map(it => it.data));
        } catch (e) {
            console.error(e);
        }
    };
    fetchData();
}, []);

the second solution will make it easier to read and will help you write code to cancel previous requests if a new one is fired or save the latest request response in state

Working codesandbox

🌐
DEV Community
dev.to › pallymore › clean-up-async-requests-in-useeffect-hooks-90h
Clean Up Async Requests in `useEffect` Hooks - DEV Community
October 12, 2019 - No, the token should be created in your useEffect call. A new token is created for every new "effect". cancel / abort is called whenever the effect re-fires (e.g. when the parameters changed, or when the component unmounts), the cleanup function is called, cancelling the previous request - in your API function you should check if a request has been aborted in your catch block and handle it accordingly.
🌐
Stack Overflow
stackoverflow.com › questions › 71969199 › how-to-use-clean-up-function-with-async-function-in-useeffect
How to use Clean up function with Async function in UseEffect?
April 22, 2022 - Anytime dependencies of an effect change, useEffect will cleanup the previous effect and run the new effect. import React, { useState, useEffect } from "react"; function App() { const [value, setValue] = useState(""); const [isCancelled, ...
🌐
Saeloun Blog
blog.saeloun.com › 2021 › 06 › 11 › react-17-runs-use-effect-cleanup-asynchronously
React 17 runs useEffect cleanup functions asynchronously | Saeloun Blog
June 11, 2021 - In React 17, the useEffect cleanup functions are delayed till the commit phase is completed. In other words, the useEffect cleanup functions run asynchronously - for example, if the component is unmounting, the cleanup runs after the screen ...
🌐
Marmelab
marmelab.com › blog › 2023 › 01 › 11 › use-async-effect-react.html
useAsyncEffect: The Missing React Hook
January 11, 2023 - But useEffect doesn’t accept asynchronous callbacks, as an async callback returns a Promise, and the return value of a side effect callback must be the cleanup function.
🌐
This Dot Labs
thisdot.co › blog › async-code-in-useeffect-is-dangerous-how-do-we-deal-with-it
Async Code in useEffect is Dangerous. How Do We Deal with It? - This Dot Labs
March 20, 2023 - If the component is unmounted before, setClient is called the client will still be created — Promises do not get cancelled just because their caller no longer exists — but without a component to manage the state setting or cleanup, it will never disconnect. This is usually quite bad. So what do we do about it? Well, it's complicated. At first glance, it looks like we can do the following, and things will be OK: ... const useClient = (user) => { const [client, setClient] = useState(null); useEffect(() => { let client; (async () => { const clientAuthToken = await fetchClientToken(user); const connection = await createWebsocketConnection(); client = await createClient(connection, clientAuthToken); setClient(client); })(); return () => { client?.disconnect(); }; }, [user]); return client; };
🌐
Medium
caesar-jd-bell.medium.com › how-to-properly-clean-up-effects-in-useeffect-9e6ab497eb02
How to Properly Clean Up Effects in useEffect | by Caesar Bell | Medium
January 3, 2023 - In this article, we will explore the various ways to properly clean up effects in useEffect. We’ll start by discussing the need for clean-up functions and then move on to more advanced techniques for cleaning up asynchronous and multiple effects. Finally, we’ll provide some best practices for ensuring proper cleanup in your own code.
🌐
HackerNoon
hackernoon.com › cleanup-functions-in-reacts-useeffect-hook-explained
Cleanup Functions in React’s UseEffect Hook — Explained with examples | HackerNoon
December 1, 2022 - Cleanup functions in React’s useEffect hook allow us to stop side effects that no longer need to be executed in the component.
🌐
DEV Community
dev.to › almustarik › why-you-cant-use-async-functions-directly-in-useeffect-and-how-to-handle-asynchronous-side-effects-in-react-5h1a
Why You Can't Use async Functions Directly in useEffect and How to Handle Asynchronous Side Effects in React - DEV Community
May 22, 2024 - Cleanup: If the effect requires cleanup (e.g., removing event listeners), the function should return a cleanup function that React will call before the component unmounts or before re-running the effect due to dependency changes.