取消异步 Axios GET 请求 - React hooks

时间:2021-04-06 14:21:07

标签: javascript reactjs axios react-hooks

我有一个 Axios GET 请求,该请求在单击按钮时开始获取 API。此请求需要很长时间(大约一分钟),我想取消它并在再次单击该按钮时启动一个新请求。

此外,如果请求处于待处理状态并且我刷新页面,我还希望能够取消该请求。

我尝试了在 SO 上找到的所有内容,但似乎没有任何效果。我还阅读了有关取消的 Axios 文档:https://github.com/axios/axios#cancellation

这一切都让我走到了那里:

更新代码(见下方编辑)

const CancelToken = axios.CancelToken;
let cancel;

function App() {
    useEffect(() => {
        async function getPairs() {
            try {
                if (cancel) {
                    cancel();
                }

                const res = await axios.get('http://localhost:3000/binance/pairs?timeframe='+timeframe+'&strat='+strat, {
                    cancelToken: new CancelToken(function executor(c) {
                    cancel = c;
                    })
                });

                if (res.status === 200 || res.data.status === 'success') {
                    setPairs(res.data.pairs);
                    setReloadConfig(false);
                }
            }
            catch (err) {
                if (axios.isCancel(err)) {
                    console.log("cancelled");
                } else {
                throw err;
                }
            }
        }
    
        // reloadConfig is a React useState hook that is set to true when clicking a button 
        if (reloadConfig) {
            getPairs();
        }
    }, [reloadConfig]);
}

export default App;

请注意,reloadConfig 是一个 React useState 钩子,在单击按钮时设置为 true,因此会触发 useEffect 钩子和 Axios 请求。

问题是,如果我多次单击该按钮,则不会取消任何请求,并且每次都会创建一个新请求。我可以看到我的终端中正在处理的所有请求。

如何在单击触发 useEffect 的按钮时取消最后一个待处理请求并创建新请求?

另外,如果我刷新页面,如何取消待处理的请求?

----- 编辑 -----

稍微修改代码后,我发现在组件函数声明之前放置 cancelTokencancel 变量让我向前迈进了一步。现在,cancel 不再是 undefined,我从 cancelled 块收到 catch 消息:

catch (err) {
    if (axios.isCancel(err)) {
        console.log("cancelled");
    }
}

如果我console.log(cancel),它返回这个:

ƒ cancel(message) {
    if (token.reason) {
      // Cancellation has already been requested
      return;
    }

    token.reason = new Cancel(message);
    resolvePromise(token.reason);
}

所以它告诉我取消成功(如果我理解正确?),但我仍然可以看到所有请求都记录在后端终端中并且仍在处理中。有什么问题吗?

非常感谢

----- 编辑 2 -----

以上显示的解决方案都有效。问题是我的一个误解:当我调用我的 API 以获取结果时,结果实际上是由一个库获取的,该库本身为每个结果获取一个 URI,这意味着对我的 API 的一次调用会多次调用另一个API 通过所述库。

我可以说社区上面展示的解决方案有效,因为我在带有“普通”模拟 API 的沙箱上尝试了它们。我认为在我的情况下发生的情况是,我与 API 一起使用的库调用了 +200 个 URL,所以我猜一个调用被取消了,但另一个 199 个调用没有。

如果有人有取消一切的解决方案,那就太好了,但我认为这更像是一种后端情况,而不是前端情况。

感谢大家的帮助和耐心。

4 个答案:

答案 0 :(得分:2)

由于您依赖 useEffect 来触发您的请求,因此您可以使用 useEffect 的清理功能在执行新的 API 请求之前取消您的 API 请求。

function App() {
    useEffect(() => {
        let CancelToken = axios.CancelToken;
        let source = CancelToken.source();
        async function getPairs() {
            try {

                const res = await axios.get('http://localhost:3000/binance/pairs?timeframe='+timeframe+'&strat='+strat, {
                    cancelToken: source.token
                });

                if (res.status === 200 || res.data.status === 'success') {
                    setPairs(res.data.pairs);
                    setReloadConfig(false);
                }
            }
            catch (err) {
                if (axios.isCancel(err)) {
                    console.log("cancelled");
                } else {
                throw err;
                }
            }
        }
    
        // reloadConfig is a React useState hook that is set to true when clicking a button 
        if (reloadConfig) {
            getPairs();
        }

        return () => {
             source.cancel('Cancelled due to stale request');
        }
    }, [reloadConfig]);
}

export default App;
<块引用>

请注意,您应该在 useEffect 中定义您的 cancel 变量 否则它将在下一次渲染时重新初始化为 undefined 如果你 直接在组件内定义。

答案 1 :(得分:0)

也许这不是网络问题,我无法给出准确答案。也许您可以考虑使用队列和广播(websocket)解决方案?

如果你想在刷新页面之前做点什么,请参考window.onbeforeunload事件。事件回调中执行取消请求的代码。

答案 2 :(得分:0)

认为您应该像这样更改实现以确保避免内存泄漏和处理错误。对 axios 请求使用单独的钩子也将解耦您的组件和请求代码。

您遇到的主要问题是您没有从 sourceconst { token: cancelToken, cancel } = source;

获得令牌
const useFetchPairs = () => {
  const [data, setData] = useState({
    pairs: null,
    isLoading: false,
    error: null,
  });
  const CancelToken = axios.CancelToken;
  const source = CancelToken.source();
  const { cancelToken: token, cancel } = source;
  const getPairs = async () => {
    setData({ pairs: null, isLoading: false, error: null });

    try {
      const res = await axios.get(
        "http://localhost:3000/binance/pairs?timeframe=" +
          timeframe +
          "&strat=" +
          strat,
        {
          cancelToken,
        }
      );

      if (res.status === 200 || res.data.status === "success") {
        setData({ pairs: res.data.pairs, isLoading: false, error: null });
      }
    } catch (err) {
      if (axios.isCancel(err)) {
        console.log("cancelled");
      } else {
        setData({ pairs: null, isLoading: false, error });
      }
    }
  };

  return [data, getPairs, cancel];
};

const MyComponent = () => {
  const [{ isLoading, pairs, error }, getPairs, cancel] = useFetchPairs();

  useEffect(() => {
    return () => {
      // cancelling when component unmounts i.e. navigation
      cancel();
    };
  }, []);

  const onClick = () => {
    if (isLoading) {
      // cancelling when button clicked and previous request in progress
      cancel();
    }
    getPairs();
  };
  return (
    <>
      {error && <p>{error}</p>}
      <div>{pairs}</div>
      <button onClick={onClick}>Click me</button>
    </>
  );
};
    

答案 3 :(得分:0)

我可以建议另一种使用自定义库 (Working Demo) 的解决方案:

import React, { useState } from "react";
import { useAsyncCallback, E_REASON_UNMOUNTED } from "use-async-effect2";
import { CanceledError } from "c-promise2";
import cpAxios from "cp-axios";

export default function TestComponent(props) {
  const [text, setText] = useState("");

  const request = useAsyncCallback(
    function* () {
      this.timeout(props.timeout);

      try {
        setText("fetching...");
        const response = yield cpAxios(props.url);
        setText(`Success: ${JSON.stringify(response.data)}`);
        // do whatever you need with the response data here 
      } catch (err) {
        CanceledError.rethrow(err, E_REASON_UNMOUNTED); //passthrough
        setText(`Failed: ${err}`);
      }
    },
    { deps: [props.url], cancelPrevious: true }
  );

  return (
    <div className="component">
      <div>{text}</div>
      <button onClick={request}>Request</button>
      <button onClick={request.cancel}>Abort</button>
    </div>
  );
}