14

The Abortcontroller signal is not working for me with Axios in React.

I wanted to replace CancelToken (as it's deprecated) with the AbortController, but it is not working, respectively the requests are not being canceled.

  let testController: AbortController;

  function loadTest() {
    testController = new AbortController();

    TestAPI.getTest(testController.signal)
      .then((e) => {
        console.log(e.data);
      })
      .catch((e) => {
        console.error(e);
      });
  }

Also in the UseEffect Cleanup I do this (here it should cancel) and also the signal's state is set to aborted, but still the request is not canceled:

  useEffect(() => () => {
    if (testController) testController.abort();
    // console.log(testController.signal.aborted) => **true**
  }, []);

Here is my API, where I pass the AbortSignal to the request:

  getTest(signal?: AbortSignal): Promise<AxiosResponse<Test[]>> {
    return axios.get(`${URI}/test`, { signal });
  },

When using Axios.CancelToken.source was working fine, but now with the AbortController, the request is never canceled.

Using: "axios": "^0.26.0",

Did someone manage to integrate the AbortController with React and Axios? Or does the AbortController only work with fetch?

bbrinck
  • 953
  • 1
  • 9
  • 33

6 Answers6

26

The axios.CancelToken API isn't deprecated as far as I can tell, it's still in the spec, but according to the docs axios also supports the AbortController of the fetch API.

Cancellation

Axios supports AbortController to abort requests in fetch API way:

const controller = new AbortController();

axios.get('/foo/bar', {
   signal: controller.signal
}).then(function(response) {
   //...
});
// cancel the request
controller.abort()

It's not clear exactly where testController is declared:

let testController: AbortController;

but I suspect it's in the body of the function component and redeclared on a subsequent render cycle.

I suggest using a React ref to store an AbortController, and reference this ref value around your app. This is so the component holds on to a stable reference of the controller from render cycle to render cycle, to be referenced in any useEffect hook cleanup function to cancel in-flight requests if/when the component unmounts.

const abortControllerRef = useRef<AbortController>(new AbortController());

function loadTest() {
  TestAPI.getTest(abortControllerRef.current.signal)
    .then((e) => {
      console.log(e.data);
    })
    .catch((e) => {
      console.error(e);
    });
}

useEffect(() => {
  const controller = abortControllerRef.current;
  return () => {
    controller.abort();
  };
}, []);
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • Why do you suggest a ref? – Silidrone Sep 09 '22 at 16:57
  • @Silidrone It is declared in the React function component scope *as a ref* so it is a stable reference from render cycle to render cycle *and* so it can be referenced in the `useEffect` cleanup function when the component unmounts. – Drew Reese Sep 09 '22 at 17:02
  • If it weren't a ref, would referencing it inside useEffect cleanup function be undefined/dangerous behaviour? – Silidrone Sep 09 '22 at 17:11
  • 1
    @Silidrone I still believe the issue OP had was declaring/redeclaring the abort controller each render cycle, so the issue was more likely that the currently declared abort controller *wasn't* the one passed to the Axios request, so calling `abort` on it wouldn't cancel the in-flight request. This is exactly the behavior the OP described, their requests not getting cancelled. – Drew Reese Sep 09 '22 at 17:13
  • 1
    @Silidrone Sorry, I suppose there was an assumed certain base level of knowledge of Javascript closures, and the React component lifecycle and hooks on my part. Thanks for asking "why" though. – Drew Reese Sep 09 '22 at 17:18
  • Ah I assumed the answer to my question, I just wanted to be certain I am not missing out on information unknown to me. The edit does improve the question for future visitors unbeknownst of such base level knowledge though, so you're welcome :) – Silidrone Sep 09 '22 at 17:20
  • Also, I had trouble with axios since AbortController is only supported >=v0.22.0 ([this pr](https://github.com/axios/axios/pull/3305)). – Silidrone Sep 09 '22 at 17:26
  • The ref solution is the way to go. But I came across an edge case whereby after aborting you still want to make a request to that endpoint. example: Let say you have a download button where a user downloads a file from a server. The user abort the first download but later changed his mind and want to download the file, since we are using the same abort signal, it terminates the request. Any suggestions on improvement @DrewReese – labs Nov 11 '22 at 17:08
  • @labs Couldn't/wouldn't the user just click the download button again to initiate another network request? In the solution above `controller.abort()` is called when the component unmounts, not when a user cancels manually. I think you might be asking about a slightly different scenario. Might make for a good new post on SO. – Drew Reese Nov 11 '22 at 18:48
  • @DrewReese Thanks, that's the correct approach. – Ishan Varshney Dec 15 '22 at 17:58
6

I would recommend to read this post.

In a nutshell you would like to use useEffect to create controller, and, what is more important, to use return statement to abort the controller.

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

 //cleanup function
 return () => {controller.abort();};
}, [fetchClick]);

getData function can then be your axious call in the form:

const getData = async (signal) =>{
 const res = await axios.get(url, {signal: signal}).then(...)
}
Alex S.
  • 974
  • 1
  • 13
  • 18
2

Abort controller often use in useEffect to fetch some data. So, in order to implement the control you can try this:

//...

const [data, setData] = useState([]);

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

  axios
    .get("https://somedata.com", { signal: controller.signal })
    .then(res => {
      setData(res.data);
    })
    .catch(err => console.log(err));

  // return cleanup function to abort request
  return () => {
    controller.abort();
  };
}, []);

//...
Haziq Musa
  • 31
  • 2
1

There's my code example, hope this helps:

useEffect(() => {
  const abortController = new AbortController();

  const getData = async () => {
    try {
      const res = await axios("/api/data/", {
        signal: abortController.signal,
      });
      const data = res.data
    } catch (error) {
      if (error.name !== "CanceledError") {
        /* Logic for non-aborted error handling goes here. */
        console.log('error:', error)
      }
    }
  };

  getData();

  // clean up function when unmounted to avoid getData fired twice problem in React 18
  return () => abortController.abort();
}, []);
Alex G
  • 1,321
  • 1
  • 24
  • 31
0

Here, I created a common Axios interceptor with AbortController.

import axios from 'axios';

const instance = axios.create({
  timeout: 25000,
  params: {},
});

/* Store requests */
const sourceRequest: Record<string, any> = {};

const controller = new AbortController();
const timeoutInterceptor = instance.interceptors.request.use(
  async (request: any) => {
    /* If the application exists cancel */
    if (sourceRequest[request.url]) {
      request.cancelToken = controller.signal;
    }

    return request;
  },
  error => {
    return Promise.reject(error);
  },
);

// Set a timeout to cancel the request
setTimeout(() => {
  instance.interceptors.request.eject(timeoutInterceptor);
  controller.abort();
}, 5000);

export const apiService = {
  request(config = {}) {
    return instance.request(config);
  },
  getData(url: string, config = {}) {
    return instance.get(url, config);
  },
  postData(url: string, data?: any, config?: Record<string, any>) {
    return instance.post(url, data, config);
  },
  putData(url: string, data?: any, config?: Record<string, any>) {
    return instance.put(url, data, config);
  },
  patchData(url: string, data?: any) {
    return instance.patch(url, data);
  },
  deleteData(url: string, config = {}) {
    return instance.delete(url, config);
  },
};
Kanti vekariya
  • 667
  • 3
  • 13
-3

All you need regarding AbortController with axios here

const controller = new AbortController();

axios.get('/foo/bar', {
   signal: controller.signal
}).then(function(response) {
   //...
});
// cancel the request
controller.abort()