在 React Hooks useEffect cleanup 中取消 Axios REST 调用失败

Posted

技术标签:

【中文标题】在 React Hooks useEffect cleanup 中取消 Axios REST 调用失败【英文标题】:Canceling an Axios REST call in React Hooks useEffects cleanup failing 【发布时间】:2019-05-20 13:49:39 【问题描述】:

我显然没有正确清理并以我应该的方式取消 axios GET 请求。在我的本地,我收到一条警告,上面写着

无法对未安装的组件执行 React 状态更新。这是 无操作,但它表明您的应用程序中存在内存泄漏。修理, 在 useEffect 清理中取消所有订阅和异步任务 功能。

在 stackblitz 上,我的代码可以工作,但由于某种原因,我无法单击按钮来显示错误。它总是显示返回的数据。

https://codesandbox.io/s/8x5lzjmwl8

请检查我的代码并找出我的缺陷。

使用AxiosFetch.js

import useState, useEffect from 'react'
import axios from 'axios'

const useAxiosFetch = url => 
    const [data, setData] = useState(null)
    const [error, setError] = useState(null)
    const [loading, setLoading] = useState(true)

    let source = axios.CancelToken.source()
    useEffect(() => 
        try 
            setLoading(true)
            const promise = axios
                .get(url, 
                    cancelToken: source.token,
                )
                .catch(function (thrown) 
                    if (axios.isCancel(thrown)) 
                        console.log(`request cancelled:$thrown.message`)
                     else 
                        console.log('another error happened')
                    
                )
                .then(a => 
                    setData(a)
                    setLoading(false)
                )
         catch (e) 
            setData(null)
            setError(e)
        

        if (source) 
            console.log('source defined')
         else 
            console.log('source NOT defined')
        

        return function () 
            console.log('cleanup of useAxiosFetch called')
            if (source) 
                console.log('source in cleanup exists')
             else 
                source.log('source in cleanup DOES NOT exist')
            
            source.cancel('Cancelling in cleanup')
        
    , [])

    return data, loading, error


export default useAxiosFetch

index.js

import React from 'react';

import useAxiosFetch from './useAxiosFetch1';

const index = () => 
    const url = "http://www.fakeresponse.com/api/?sleep=5&data=%22Hello%22:%22World%22";
    const data,loading = useAxiosFetch(url);

    if (loading) 
        return (
            <div>Loading...<br/>
                <button onClick=() => 
                    window.location = "/awayfrom here";
                 >switch away</button>
            </div>
        );
     else 
        return <div>JSON.stringify(data)xx</div>
    
;

export default index;

【问题讨论】:

【参考方案1】:

完全可取消的例程示例,您根本不需要任何 CancelToken (Play with it here):

import React,  useState  from "react";
import  useAsyncEffect, E_REASON_UNMOUNTED  from "use-async-effect2";
import  CanceledError  from "c-promise2";
import cpAxios from "cp-axios"; // cancellable axios wrapper

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

  const cancel = useAsyncEffect(
    function* () 
      console.log("mount");

      this.timeout(props.timeout);
   
      try 
        setText("fetching...");
        const response = yield cpAxios(props.url);
        setText(`Success: $JSON.stringify(response.data)`);
       catch (err) 
        CanceledError.rethrow(err, E_REASON_UNMOUNTED); //passthrough
        setText(`Failed: $err`);
      

      return () => 
        console.log("unmount");
      ;
    ,
    [props.url]
  );

  return (
    <div className="component">
      <div className="caption">useAsyncEffect demo:</div>
      <div>text</div>
      <button onClick=cancel>Abort</button>
    </div>
  );

【讨论】:

【参考方案2】:

这就是我的做法,我认为它比这里的其他答案要简单得多:

import React,  Component  from "react";
import axios from "axios";

export class Example extends Component 
    _isMounted = false;

    componentDidMount() 
        this._isMounted = true;

        axios.get("/data").then((res) => 
            if (this._isMounted && res.status === 200) 
                // Do what you need to do
            
        );
    

    componentWillUnmount() 
        this._isMounted = false;
    

    render() 
        return <div></div>;
    


export default Example;

【讨论】:

OP 想使用 hooks 和 axios 取消令牌,这里忽略了。【参考方案3】:

这是最终代码,万一其他人回来,一切正常。

import useState, useEffect from "react";
import axios, AxiosResponse from "axios";

const useAxiosFetch = (url: string, timeout?: number) => 
    const [data, setData] = useState<AxiosResponse | null>(null);
    const [error, setError] = useState(false);
    const [errorMessage, setErrorMessage] = useState(null);
    const [loading, setLoading] = useState(true);

    useEffect(() => 
        let unmounted = false;
        let source = axios.CancelToken.source();
        axios.get(url, 
            cancelToken: source.token,
            timeout: timeout
        )
            .then(a => 
                if (!unmounted) 
                    // @ts-ignore
                    setData(a.data);
                    setLoading(false);
                
            ).catch(function (e) 
            if (!unmounted) 
                setError(true);
                setErrorMessage(e.message);
                setLoading(false);
                if (axios.isCancel(e)) 
                    console.log(`request cancelled:$e.message`);
                 else 
                    console.log("another error happened:" + e.message);
                
            
        );
        return function () 
            unmounted = true;
            source.cancel("Cancelling in cleanup");
        ;
    , [url, timeout]);

    return data, loading, error, errorMessage;
;

export default useAxiosFetch;

【讨论】:

很酷的解决方案!当我们取消卸载请求时,我们真的需要在then 中检查unmounted 吗? @fabb unmounted 变量是多余的,因为useEffect() 中的返回函数已经设计为在组件卸载时运行。来源:reactjs.org/docs/hooks-effect.html#example-using-hooks-1 如果请求没有取消或者其他一些任务不能取消,需要卸载。如果您在网络调用返回之前降低网络速度并卸载组件,您将看到错误“无法对卸载的组件执行 React 状态更新。” 谢谢!你帮了我很多【参考方案4】:

您的问题是,在快速网络上,请求会快速响应,并且不允许您单击按钮。在您可以通过 ChromeDevTools 实现的节流网络上,您可以正确地可视化此行为

其次,当您尝试使用 window.location.href = 'away link' 导航离开时,react 不会触发/执行组件清理,因此不会触发 useEffect 的清理功能。

利用路由器工作

import React from 'react'
import ReactDOM from 'react-dom'
import BrowserRouter as Router, Switch, Route from 'react-router-dom'

import useAxiosFetch from './useAxiosFetch'

function App(props) 
  const url = 'https://www.siliconvalley-codecamp.com/rest/session/arrayonly'
  const data, loading = useAxiosFetch(url)

  // setTimeout(() => 
  //   window.location.href = 'https://www.google.com/';
  // , 1000)
  if (loading) 
    return (
      <div>
        Loading...
        <br />
        <button
          onClick=() => 
            props.history.push('/home')
          
        >
          switch away
        </button>
      </div>
    )
   else 
    return <div>JSON.stringify(data)</div>
  


ReactDOM.render(
  <Router>
    <Switch>
      <Route path="/home" render=() => <div>Hello</div> />
      <Route path="/" component=App />
    </Switch>
  </Router>,
  document.getElementById('root'),
)

您可以check the demo 在慢速网络上正常工作

【讨论】:

以上是关于在 React Hooks useEffect cleanup 中取消 Axios REST 调用失败的主要内容,如果未能解决你的问题,请参考以下文章

React Hooks --- useState 和 useEffect

如何使用 React hooks 和 Redux 从 useEffect 执行 store.unsubscribe

React hooks useEffect fetch CORS 问题

react hooks 中的 useEffect, useCallback, useMemo

react hooks 中的 useEffect, useCallback, useMemo

react hooks 中的 useEffect, useCallback, useMemo