【问题标题】:Correct way to cancel async axios request in a React functional component在 React 功能组件中取消异步 axios 请求的正确方法
【发布时间】:2020-10-13 10:27:15
【问题描述】:

在 React 功能组件中取消异步请求的正确方法是什么?

我有一个脚本在加载时(或在某些用户操作下)从 API 请求数据,但如果这是在执行过程中并且用户导航离开,则会导致以下警告:

警告:无法对未安装的组件执行 React 状态更新。这是一个空操作,但它表明您的应用程序中存在内存泄漏。要解决此问题,请在 useEffect 清理函数中取消所有订阅和异步任务。

我读过的大部分内容都通过基于类的组件的componentDidUnmount 方法中的AbortController 解决了这个问题。然而,我的 React 应用程序中有一个功能组件,它使用 Axois 向 API 发出异步数据请求。

函数驻留在功能组件中的useEffect钩子中,以确保在组件呈现时函数运行:

  useEffect(() => {
    loadFields();
  }, [loadFields]);

这是它调用的函数:

  const loadFields = useCallback(async () => {
    setIsLoading(true);
    try {
      await fetchFields(
        fieldsDispatch,
        user.client.id,
        user.token,
        user.client.directory
      );
      setVisibility(settingsDispatch, user.client.id, user.settings);
      setIsLoading(false);
    } catch (error) {
      setIsLoading(false);
    }
  }, [
    fieldsDispatch,
    user.client.id,
    user.token,
    user.client.directory,
    settingsDispatch,
    user.settings,
  ]);

这是触发的axios请求:


async function fetchFields(dispatch, clientId, token, folder) {
  try {
    const response = await api.get(clientId + "/fields", {
      headers: { Authorization: "Bearer " + token },
    });

    // do something with the response

  } catch (e) {
    handleRequestError(e, "Failed fetching fields: ");
  }
}

注意:api 变量是对 axios.create 对象的引用。

【问题讨论】:

    标签: javascript reactjs axios


    【解决方案1】:

    使用axios 取消fetch 操作:

    1. Cancel 具有给定源令牌的请求
    2. 确保在卸载组件后不要更改组件状态

    广告 1。)

    axios 自带取消 API:

    const source = axios.CancelToken.source();
    axios.get('/user/12345', { cancelToken: source.token })
    source.cancel(); // invoke to cancel request
    

    您可以通过停止不再需要的异步请求来使用它来优化性能。对于原生浏览器fetch API,将使用AbortController

    广告 2。)

    这将停止警告"Warning: Can't perform a React state update on an unmounted component."。例如。您不能在已卸载的组件上调用 setStateHere 是 Hook 实施和封装上述约束的示例。


    示例:useAxiosFetch

    我们可以将这两个步骤合并到一个自定义 Hook 中:

    function useAxiosFetch(url, { onFetched, onError, onCanceled }) {
      React.useEffect(() => {
        const source = axios.CancelToken.source();
        let isMounted = true;
        axios
          .get(url, { cancelToken: source.token })
          .then(res => { if (isMounted) onFetched(res); })
          .catch(err => {
            if (!isMounted) return; // comp already unmounted, nothing to do
            if (axios.isCancel(err)) onCanceled(err);
            else onError(err);
          });
    
        return () => {
          isMounted = false;
          source.cancel();
        };
      }, [url, onFetched, onError, onCanceled]);
    }
    

    import React from "react";
    
    import axios from "axios";
    
    export default function App() {
      const [mounted, setMounted] = React.useState(true);
      return (
        <div>
          {mounted && <Comp />}
          <button onClick={() => setMounted(p => !p)}>
            {mounted ? "Unmount" : "Mount"}
          </button>
        </div>
      );
    }
    
    const Comp = () => {
      const [state, setState] = React.useState("Loading...");
      const url = `https://jsonplaceholder.typicode.com/users/1?_delay=3000&timestamp=${new Date().getTime()}`;
      const handlers = React.useMemo(
        () => ({
          onFetched: res => setState(`Fetched user: ${res.data.name}`),
          onCanceled: err => setState("Request canceled"),
          onError: err => setState("Other error:", err.message)
        }),
        []
      );
      const cancel = useAxiosFetch(url, handlers);
    
      return (
        <div>
          <p>{state}</p>
          {state === "Loading..." && (
            <button onClick={cancel}>Cancel request</button>
          )}
        </div>
      );
    };
    
    // you can extend this hook with custom config arg for futher axios options
    function useAxiosFetch(url, { onFetched, onError, onCanceled }) {
      const cancelRef = React.useRef();
      const cancel = () => cancelRef.current && cancelRef.current.cancel();
    
      React.useEffect(() => {
        cancelRef.current = axios.CancelToken.source();
        let isMounted = true;
        axios
          .get(url, { cancelToken: cancelRef.current.token })
          .then(res => {
            if (isMounted) onFetched(res);
          })
          .catch(err => {
            if (!isMounted) return; // comp already unmounted, nothing to do
            if (axios.isCancel(err)) onCanceled(err);
            else onError(err);
          });
    
        return () => {
          isMounted = false;
          cancel();
        };
      }, [url, onFetched, onError, onCanceled]);
      return cancel;
    }

    【讨论】:

    • 抱歉耽误了时间!我接受了@ford04 的回答,因为它包含了 Axios 的细节。
    • @theblackgigant 的回答对那些使用 react 组件钩子方法进行生命周期管理的人也很有帮助
    • 这个例子总是在组件安装后立即请求数据。当请求由用户操作(即按钮单击)触发时,您会使用什么方法?
    • 您可以在事件处理程序中直接调用axios.get(...),添加一个可变引用(useRef),它存储有关挂载状态的信息(isMounted,与上述相同的逻辑)和取消令牌source。如果组件卸载,请在单独的 useEffect 中调用 source.cancel();
    【解决方案2】:

    useEffect 有一个可以使用的返回选项。它的行为(几乎)与 componentDidUnmount 相同。

    useEffect(() => {
      // Your axios call
    
      return () => {
        // Your abortController
      }
    }, []);
    

    【讨论】:

      【解决方案3】:

      您可以使用 lodash.debounce 并尝试以下步骤

      第 1 步:

      inside constructor:
      this.state{
       cancelToken: axios.CancelToken,
       cancel: undefined,
      }
      this.doDebouncedTableScroll = debounce(this.onScroll, 100);
      

      第 2 步: 使用 axios 添加的内部函数:

      if (this.state.cancel !== undefined) {
                      cancel();
                  }
      

      第 3 步:

       onScroll = ()=>{
          axiosInstance()
               .post(`xxxxxxx`)
                    , {data}, {
                        cancelToken: new cancelToken(function executor(c) {
                               this.setState({ cancel: c });
                              })
                         })
                          .then((response) => {
          
                               }
      

      【讨论】: