如何将AbortController与Axios和React集成?

15

在React中,使用Axios时AbortController信号对我不起作用。

我希望使用AbortController替换已弃用的CancelToken,但它无法正常工作,即请求未被取消。

  let testController: AbortController;

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

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

在UseEffect清除函数中,我这样做(这里应该取消),并且信号的状态也被设置为aborted,但请求仍然没有被取消:

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

这是我的API,我将AbortSignal传递给请求:

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

使用 Axios.CancelToken.source 时一切正常,但现在使用 AbortController 后,请求永远不会被取消。

正在使用的版本是:"axios": "^0.26.0"

是否有人成功地将 AbortController 与 React 和 Axios 集成?或者 AbortController只能与 fetch 一起使用?

6个回答

27

据我所知,axios.CancelToken API并没有被废弃,它仍在规范中存在,但根据文档,axios还支持fetch APIAbortController

取消请求

Axios支持AbortController以类似于fetch API的方式来中止请求:

const controller = new AbortController();

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

目前并不清楚 testController 是在哪里声明的:

let testController: AbortController;

但我怀疑它在函数组件的主体中,并且在后续渲染周期中重新声明。

我建议使用React ref来存储AbortController,并在应用程序中引用此ref值。这样,组件可以从渲染周期到渲染周期保持控制器的稳定引用,并在任何useEffect清理函数中引用以取消正在运行的请求(如果/当组件卸载时)。

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();
  };
}, []);

1
@Silidrone 它在 React 函数组件作用域中 作为引用 声明,因此它是从渲染周期到渲染周期的稳定引用,并且可以在组件卸载时在 useEffect 清理函数中引用。 - Drew Reese
如果它不是一个引用,那么在 useEffect 的清理函数中引用它会是未定义的/危险的行为吗? - Silidrone
1
@Silidrone,我仍然相信OP遇到的问题是在每个渲染周期中声明/重新声明abort controller,因此问题更可能是当前声明的abort controller 不是传递给Axios请求的那个,因此在其上调用abort将无法取消正在进行的请求。这正是OP所描述的行为,他们的请求没有被取消。 - Drew Reese
1
@Silidrone 抱歉,我假设你已经具备了一定的 JavaScript 闭包、React 组件生命周期和钩子的知识。不过还是感谢你问出了“为什么”。 - Drew Reese
ref解决方案是正确的选择。但我遇到了一个边缘情况,即在中止后仍然希望向该端点发出请求。例如:假设您有一个下载按钮,用户从服务器下载文件。用户中止了第一次下载,但后来改变了主意并想要下载文件,由于我们使用相同的中止信号,它终止了请求。对于改进有任何建议吗@DrewReese - labs
显示剩余5条评论

6

我建议阅读这篇文章

简而言之,您可以使用useEffect创建控制器,更重要的是使用return语句来中止该控制器。

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

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

getData函数可以作为您的axios调用表单:

const getData = async (signal) =>{
 const res = await axios.get(url, {signal: signal}).then(...)
}

2

Abort controller经常在useEffect中用于获取一些数据。因此,为了实现控制,您可以尝试以下操作:

//...

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();
  };
}, []);

//...

1
这是我的代码示例,希望能对你有所帮助:
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();
}, []);

1
在这里,我使用AbortController创建了一个常见的Axios拦截器。
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);
  },
};

-4

关于使用axios的AbortController,你需要了解的一切 在这里

const controller = new AbortController();

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


2
这并没有真正回答如何将其集成到React的问题。 - sasha_gud

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接