🌐
j-labs
j-labs.pl › home › tech blog › how to use the useeffect hook with the abortcontroller
AbortController in React. How to use the useEffect hook with the AbortControl? | j‑labs
December 9, 2025 - The AbortController interface provides a standardized mechanism to cancel ongoing asynchronous operations in JavaScript. Integrating AbortController with React’s useEffect hook prevents memory leaks by canceling requests when components unmount.
🌐
Medium
medium.com › @icjoseph › using-react-to-understand-abort-controllers-eb10654485df
Using React to understand Abort Controllers | by Joseph Chamochumbi | Medium
November 18, 2022 - And there’s the AbortController, which fits both cases. You’d use this in a React useEffect hook like this:
Discussions

Using useEffect hook with "async"
I read that in need to enter in my hook AbortController but I don't know how. I using next.js. What are best methods to eliminate this problem ? And I get this 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 ... More on stackoverflow.com
🌐 stackoverflow.com
June 4, 2020
How to use Abort Controller to abort APIs initiated from inside useEffect but triggered by user action?
function TestComponent(props) { const ref = React.useRef(null); React.useEffect(() => { ref.current= new AbortController; return () => (ref.current.abort()); }, []); const cancelAllCalls= ()=>{ ref.current.abort(); ref.current= new AbortController(); } // your hooks React.useEffect(() => { ... More on stackoverflow.com
🌐 stackoverflow.com
How to use the AbortController to cancel Promises in React?
1 How to use Abort Controller to abort APIs initiated from inside useEffect but triggered by user action? 21 How to integrate AbortController with Axios and React? More on stackoverflow.com
🌐 stackoverflow.com
Bug: React.StrictMode causes AbortController to cancel
React version: 18.2 Steps To Reproduce import a hook that uses AbortController Without React.StrictMode the abort controller aborted = false With React.StrictMode the abort controller gets set to a... More on github.com
🌐 github.com
15
September 16, 2022
🌐
Close
making.close.com › posts › introducting-use-abortable-effect-react-hook
Introducing useAbortableEffect: a simple React hook for running abortable effects | The Making of Close
June 18, 2020 - API is compatible with useEffect, where the effect function you pass-in accepts an AbortSignal instance as a param and you can return a cleanup function that accepts an AbortController instance. Supports abortable fetch requests. Supports running custom operations/computations that can be easily ...
🌐
Medium
eminfurkan.medium.com › managing-asynchronous-operations-with-abortcontroller-in-react-e9bec3565ec8
Managing Asynchronous Operations with AbortController in React | by Furkan Tezeren | Medium
July 30, 2023 - We also clean up by canceling the request using abortController.abort() when the component unmounts. ... import { useEffect, useState } from 'react'; function MyComponent() { const [data, setData] = useState(null); const controller = new AbortController(); const signal = controller.signal; useEffect(() => { async function fetchData() { try { const response = await fetch('https://my-api.com/endpoint', { signal }); const json = await response.json(); setData(json); } catch (error) { if (error.name === 'AbortError') { console.log('Fetching data was aborted'); } else { console.error(error); } } } fetchData(); return () => { controller.abort(); }; }, []); return ( <div> {data ?
🌐
LogRocket
blog.logrocket.com › home › understanding react’s useeffect cleanup function
Understanding React’s useEffect cleanup function - LogRocket Blog
December 16, 2024 - Use the AbortController API for new projects. Let’s see an example of when the above warning can happen and how to use the cleanup function when it does. Let’s begin by creating two files: Post and App. Continue by writing the following code: // 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 ?
Top answer
1 of 1
7

Using an abort controller, in its rawest form:

const controller = new AbortController();
const { signal } = controller;
...

fetch(url, { signal });

...
// abort
controller.abort();

To abort an in-flight fetch in effect hook

useEffect(() => {
  const controller = new AbortController();
  const { signal } = controller;
  fetch(url, { signal });

  return () => {
    controller.abort(); // abort on unmount for cleanup
  };
}, []);

I found this article very informative when I needed to develop a way to cancel fetch requests.

The signal needs to be added to the fetch requests options object. You can also define the async fetchData function inside the effect (this is normal), so it's all enclosed in the effect hook's callback scope.

export const useMovieDetailsFetch = (movieId) => {
  const [state, setState] = useState({})
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(false)

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

    const fetchData = async () => {
      setError(false);
      setLoading(true);

      try {
        const movieDetailsEndpoint =
          `${MOVIE_API_URL}movie/${movieId}?api_key=${MOVIE_KEY}`;
        const result =
          await (await fetch(movieDetailsEndpoint, { signal })).json();
        const creditsEndpoint =
          `${MOVIE_API_URL}movie/${movieId}/credits?api_key=${MOVIE_KEY}`;
        const creditsResult =
          await (await fetch(creditsEndpoint, { signal })).json();
        // Filtring in crew for directors only
        const movieDirectors = creditsResult.crew.filter(
          (member) => member.job === 'Director'
        );

        setState({
          ...result,
          movieDirectors,
          actors: creditsResult.cast,
        });
      } catch (error) {
        setError(true);
      }
      setLoading(false);
    }

    fetchData();

    return () => controller.abort();
  }, [movieId]);

  return [state, loading, error];
}
🌐
DEV Community
dev.to › bil › using-abortcontroller-with-react-hooks-and-typescript-to-cancel-window-fetch-requests-1md4
Using AbortController (with React Hooks and TypeScript) to cancel window.fetch requests - DEV Community
March 27, 2019 - // src/hooks/useProfileInformation.jsx import {useState, useEffect} from 'react' export function useProfileInformation({accessToken}) { const [profileInfo, setProfileInfo] = useState(null) useEffect(() => { const abortController = new AbortController() window .fetch('https://api.example.com/v1/me', { headers: {Authorization: `Bearer ${accessToken}`}, method: 'GET', mode: 'cors', signal: abortController.signal, }) .then(res => res.json()) .then(res => setProfileInfo(res.profileInfo)) return function cancel() { abortController.abort() } }, [accessToken]) return profileInfo } // src/app.jsx impor
Find elsewhere
🌐
Academind
academind.com › tutorials › useeffect-abort-http-requests
useEffect(), Http Requests & Aborting
July 9, 2020 - Instead of preventing state updates by using a variable, we could cancel the Http request, made via fetch(), by using a built-in JavaScript object called AbortController (MDN docs).
🌐
Wanago
wanago.io › home › using abortcontroller to deal with race conditions in react
Using AbortController to deal with race conditions in React
April 11, 2022 - The most straightforward fix for the above issue is introducing a didCancel variable, as suggested by Dan Abramov. When doing that, we need to use the fact that we can clean up after our useEffect hook.
🌐
GitHub
gist.github.com › kentcdodds › b36572b6e9227207e6c71fd80e63f3b4
abort-controller.js · GitHub
function useAbortController() { const abortControllerRef = React.useRef<AbortController>() const getAbortController = () => { return (abortControllerRef.current = abortControllerRef.current || new AbortController()) } React.useEffect(() => { return () => getAbortController().abort() }, []) const getSignal = React.useCallback(() => getAbortController().signal, []) return getSignal }
Top answer
1 of 3
22

Based off your code, there are a few corrections to make:

Don't return new Promise() inside an async function

You use new Promise if you're taking something event-based but naturally asynchronous, and wrap it into a Promise. Examples:

  • setTimeout
  • Web Worker messages
  • FileReader events

But in an async function, your return value will already be converted to a promise. Rejections will automatically be converted to exceptions you can catch with try/catch. Example:

async function MyAsyncFunction(): Promise<number> {
  try {
    const value1 = await functionThatReturnsPromise(); // unwraps promise 
    const value2 = await anotherPromiseReturner();     // unwraps promise
    if (problem)
      throw new Error('I throw, caller gets a promise that is eventually rejected')
    return value1 + value2; // I return a value, caller gets a promise that is eventually resolved
  } catch(e) {
    // rejected promise and other errors caught here
    console.error(e);
    throw e; // rethrow to caller
  }
}

The caller will get a promise right away, but it won't be resolved until the code hits the return statement or a throw.

What if you have work that needs to be wrapped with a Promise constructor, and you want to do it from an async function? Put the Promise constructor in a separate, non-async function. Then await the non-async function from the async function.

function wrapSomeApi() {
  return new Promise(...);
}

async function myAsyncFunction() {
  await wrapSomeApi();
}

When using new Promise(...), the promise must be returned before the work is done

Your code should roughly follow this pattern:

function MyAsyncWrapper() {
  return new Promise((resolve, reject) => {
    const workDoer = new WorkDoer();
    workDoer.on('done', result => resolve(result));
    workDoer.on('error', error => reject(error));
    // exits right away while work completes in background
  })
}

You almost never want to use Promise.resolve(value) or Promise.reject(error). Those are only for cases where you have an interface that needs a promise but you already have the value.

AbortController is for fetch only

The folks that run TC39 have been trying to figure out cancellation for a while, but right now there's no official cancellation API.

AbortController is accepted by fetch for cancelling HTTP requests, and that is useful. But it's not meant for cancelling regular old work.

Luckily, you can do it yourself. Everything with async/await is a co-routine, there's no pre-emptive multitasking where you can abort a thread or force a rejection. Instead, you can create a simple token object and pass it to your long running async function:

const token = { cancelled: false }; 
await doLongRunningTask(params, token); 

To do the cancellation, just change the value of cancelled.

someElement.on('click', () => token.cancelled = true); 

Long running work usually involves some kind of loop. Just check the token in the loop, and exit the loop if it's cancelled

async function doLongRunningTask(params: string, token: { cancelled: boolean }) {
  for (const task of workToDo()) {
    if (token.cancelled)
      throw new Error('task got cancelled');
    await task.doStep();
  }
}

Since you're using react, you need token to be the same reference between renders. So, you can use the useRef hook for this:

function useCancelToken() {
  const token = useRef({ cancelled: false });
  const cancel = () => token.current.cancelled = true;
  return [token.current, cancel];
}

const [token, cancel] = useCancelToken();

// ...

return <>
  <button onClick={ () => doLongRunningTask(token) }>Start work</button>
  <button onClick={ () => cancel() }>Cancel</button>
</>;

hash-wasm is only semi-async

You mentioned you were using hash-wasm. This library looks async, as all its APIs return promises. But in reality, it's only await-ing on the WASM loader. That gets cached after the first run, and after that all the calculations are synchronous.

Even if it is wrapped in an async function or in a function returning a Promise, code must yield the thread to act concurrently, which hash-wasm does not appear to do in its main computation loop.

So how can you let your code breath if you've got CPU intensive code like what hash-wasm uses? You can do your work in increments, and schedule those increments with setTimeout:

for (const step of stepsToDo) {
  if (token.cancelled)
    throw new Error('task got cancelled');

  // schedule the step to run ASAP, but let other events process first
  await new Promise(resolve => setTimeout(resolve, 0));

  const chunk = await loadChunk();
  updateHash(chunk);
}

(Note that I'm using a Promise constructor here, but awaiting immediately instead of returning it)

The technique above will run slower than just doing the task. But by yielding the thread, stuff like React updates can execute without an awkward hang.

If you really need performance, check out Web Workers, which let you do CPU-heavy work off-thread so it doesn't block the main thread. Libraries like workerize can help you convert async functions to run in a worker.


That's everything I have for now, I'm sorry for writing a novel

2 of 3
0

I can suggest my library (use-async-effect2) for managing the cancellation of asynchronous tasks/promises. Here is a simple demo with nested async function cancellation:

    import React, { useState } from "react";
    import { useAsyncCallback } from "use-async-effect2";
    import { CPromise } from "c-promise2";
    
    // just for testing
    const factorialAsync = CPromise.promisify(function* (n) {
      console.log(`factorialAsync::${n}`);
      yield CPromise.delay(500);
      return n != 1 ? n * (yield factorialAsync(n - 1)) : 1;
    });
    
    function TestComponent({ url, timeout }) {
      const [text, setText] = useState("");
    
      const myTask = useAsyncCallback(
        function* (n) {
          for (let i = 0; i <= 5; i++) {
            setText(`Working...${i}`);
            yield CPromise.delay(500);
          }
          setText(`Calculating Factorial of ${n}`);
          const factorial = yield factorialAsync(n);
          setText(`Done! Factorial=${factorial}`);
        },
        { cancelPrevious: true }
      );
    
      return (
        <div>
          <div>{text}</div>
          <button onClick={() => myTask(15)}>
            Run task
          </button>
          <button onClick={myTask.cancel}>
            Cancel task
          </button>
        </div>
      );
    }
🌐
Westbrookdaniel
westbrookdaniel.com › blog › react-abort-controllers
Using AbortControllers to Cancel Fetch in React - Daniel Westbrook
useEffect(() => { const controller = new AbortController(); fetch("https://jsonplaceholder.typicode.com/posts/1", { signal: controller.signal, }) .then((res) => res.json()) .then((json) => setMessage(json.title)) .catch((error) => console.error(error.message)); return () => controller.abort(); }, []);
🌐
GitHub
github.com › facebook › react › issues › 25284
Bug: React.StrictMode causes AbortController to cancel · Issue #25284 · facebook/react
September 16, 2022 - With React.StrictMode the abort controller gets set to aborted = true · const useAbortController = (abortControllerProp, shouldAutoRestart = false) => { const abortController = useRef(abortControllerProp || initAbortController()); useEffect(() => { if (shouldAutoRestart && abortController.current.signal.aborted) { abortController.current = initAbortController(); } }, [abortController.current.signal.aborted, shouldAutoRestart]); useEffect(() => () => abortController.current.abort(), []); return abortController.current; }; The "echoed" rendering of the component causes the the controller to go from aborted false -> true.
Author   EdmundsEcho
Top answer
1 of 1
4

This occurs because useEffect runs twice when Strict Mode is enabled, or to be more specific, useEffect runs after a component gets mounted. With Strict Mode enabled, React components get mounted, unmounted and then mounted again, effectively causing useEffect to run twice.

After the first run, the loading state is set to false and it is expected, that the subsequent second run sets the loading state again to true. However, because the fetchData function runs asynchronically, the second useEffect run starts and sets loading to true (while it is still true) before the first run of the fetchData function finishes, and before (!) loading state will be set to false by the first run.

So loading is set to false while the second run is fetching data, but after the second run sets loading to true.

The reason why controller.abort() appears to mess with your loading state is that, without aborting, the first run finishes after (!) fetching data, then setting loading to false (while the second run already started, but that doesn't change the outcome) and everything seems fine as data is fetched correctly and loading is set to false by the first run.

However, when aborting, the first run finishes much earlier, because it doesn't wait for the fetch, setting the loading state to false (finally block), while the second run already started and is fetching data. Thus while (!) the second run is fetching data the loading state is set to false, although the second run is still fetching/"loading" and there is no fetched data from the first run nor from the second run.

A possible solution could be to set the loading state to false only when the request was not aborted by the controller. This can be determined by the abortController.signal.aborted boolean.

} finally {
  if (!abortController.signal.aborted) setLoading(false)
}

But this should be used with caution as this can cause an infinite loading state if a request is aborted, but no second request is triggered which would set the loading state to false again.

Another possible solution would be to discard the loading state and use an explicit error state. Loading would be determined by deriving it from the availability of the fetched data and the absence of an error.

const [data, setData] = useState<MyDataType[]>([])
const [hasError, setHasError] = useState(false)
//...

// using data.length here to determine if data has been fetched correctly
const isLoading = !data.length && !hasError
return (
    <>
      <Table
        columns={columns}
        rowKey={(record) => record.id}
        dataSource={data}
        size="small"
        pagination={{pageSize: 50}}
        loading={isLoading}
      />
    </>
  )

Initially data (or in this case data.length) is falsy, and hasError is false, so isLoading will be true. As soon as one of the state variables (data or hasError) gets set to something truthy (causing a rerendering), isLoading will be false.

A third solution would be to leave it as is, because Strict Mode does not affect production, so useEffect will not run twice in production, and the loading state will be set correctly.

🌐
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 - When is if(!isCancelled) inside the useEffect is being checked? ... Sr. FE Dev @ AWS - comments/blogs are my own. ... Sr. FE Dev @ AWS ... In your case using the flag is not going to work. If you are using fetch to make requests you can use AbortController (just provide the signal to the thunk action)
🌐
DEV Community
dev.to › leapcell › do-you-really-know-abortcontroller-3628
Do You Really Know AbortController? - DEV Community
January 16, 2025 - For older browsers, consider adding a polyfill to support AbortController. In React, effects can inadvertently run in parallel if the component updates before a previous asynchronous task completes: function FooComponent({ something }) { useEffect(async () => { const data = await fetch(url + something); // Handle the data }, [something]); return <>...</>; } To avoid such issues, use AbortController to cancel previous tasks: function FooComponent({ something }) { useEffect(() => { const controller = new AbortController(); const { signal } = controller; (async () => { const data = await fetch(ur