如何取消对 componentWillUnmount 的提取

IT技术 reactjs
2021-04-20 00:23:55

我认为标题说明了一切。每次卸载仍在获取的组件时都会显示黄色警告。

安慰

警告:无法在未安装的组件上调用setState(或forceUpdate)。这是一个空操作,但是......要修复,取消方法中的所有订阅和异步任务componentWillUnmount

  constructor(props){
    super(props);
    this.state = {
      isLoading: true,
      dataSource: [{
        name: 'loading...',
        id: 'loading',
      }]
    }
  }

  componentDidMount(){
    return fetch('LINK HERE')
      .then((response) => response.json())
      .then((responseJson) => {
        this.setState({
          isLoading: false,
          dataSource: responseJson,
        }, function(){
        });
      })
      .catch((error) =>{
        console.error(error);
      });
  }
6个回答

当您触发 Promise 时,它​​可能需要几秒钟才能解决,届时用户可能已导航到您应用程序中的另一个位置。因此,当setState在未安装的组件上执行Promise resolves 并且您收到错误时 - 就像您的情况一样。这也可能导致内存泄漏。

这就是为什么最好将一些异步逻辑移出组件的原因。

否则,您将需要以某种方式取消您的 Promise或者 - 作为最后的手段(这是一种反模式) - 您可以保留一个变量来检查组件是否仍然安装:

componentDidMount(){
  this.mounted = true;

  this.props.fetchData().then((response) => {
    if(this.mounted) {
      this.setState({ data: response })
    }
  })
}

componentWillUnmount(){
  this.mounted = false;
}

我将再次强调这一点 - 这是一种反模式,但在您的情况下可能就足够了(就像他们对Formik实现所做的那样)。

GitHub上的类似讨论

编辑:

这可能是我如何使用Hooks解决同样的问题(只有 React)

选项 A:

import React, { useState, useEffect } from "react";

export default function Page() {
  const value = usePromise("https://something.com/api/");
  return (
    <p>{value ? value : "fetching data..."}</p>
  );
}

function usePromise(url) {
  const [value, setState] = useState(null);

  useEffect(() => {
    let isMounted = true; // track whether component is mounted

    request.get(url)
      .then(result => {
        if (isMounted) {
          setState(result);
        }
      });

    return () => {
      // clean up
      isMounted = false;
    };
  }, []); // only on "didMount"

  return value;
}

选项 B:或者,useRef它的行为类似于类的静态属性,这意味着当它的值发生变化时,它不会重新渲染组件:

function usePromise2(url) {
  const isMounted = React.useRef(true)
  const [value, setState] = useState(null);


  useEffect(() => {
    return () => {
      isMounted.current = false;
    };
  }, []);

  useEffect(() => {
    request.get(url)
      .then(result => {
        if (isMounted.current) {
          setState(result);
        }
      });
  }, []);

  return value;
}

// or extract it to custom hook:
function useIsMounted() {
  const isMounted = React.useRef(true)

  useEffect(() => {
    return () => {
      isMounted.current = false;
    };
  }, []);

  return isMounted; // returning "isMounted.current" wouldn't work because we would return unmutable primitive
}

示例:https : //codesandbox.io/s/86n1wq2z8

哦,我之前没有注意到您的答案的代码,它确实有效。谢谢
2021-05-29 00:23:55
所以没有真正的方法可以取消对 componentWillUnmount 的获取?
2021-06-05 00:23:55
@Karpik 我的意思是使用 redux 或 mobx 或其他状态管理库。然而,诸如 react-suspense 之类的新功能可能会解决它。
2021-06-05 00:23:55
“这就是为什么最好将异步逻辑移出组件的原因。”是什么意思?反应中的一切不是一个组件吗?
2021-06-11 00:23:55
2021-06-17 00:23:55

React 的友好人士建议将您的 fetch 调用/Promise包装在可取消的Promise中。虽然该文档中没有建议将代码与具有 fetch 的类或函数分开,但这似乎是可取的,因为其他类和函数可能需要此功能,代码重复是一种反模式,无论代码是否存在应在 中处理或取消componentWillUnmount()根据 React,您可以调用cancel()包装好的 PromisecomponentWillUnmount以避免在未安装的组件上设置状态。

如果我们使用 React 作为指导,所提供的代码将类似于以下代码片段:

const makeCancelable = (promise) => {
    let hasCanceled_ = false;

    const wrappedPromise = new Promise((resolve, reject) => {
        promise.then(
            val => hasCanceled_ ? reject({isCanceled: true}) : resolve(val),
            error => hasCanceled_ ? reject({isCanceled: true}) : reject(error)
        );
    });

    return {
        promise: wrappedPromise,
        cancel() {
            hasCanceled_ = true;
        },
    };
};

const cancelablePromise = makeCancelable(fetch('LINK HERE'));

constructor(props){
    super(props);
    this.state = {
        isLoading: true,
        dataSource: [{
            name: 'loading...',
            id: 'loading',
        }]
    }
}

componentDidMount(){
    cancelablePromise.
        .then((response) => response.json())
        .then((responseJson) => {
            this.setState({
                isLoading: false,
                dataSource: responseJson,
            }, () => {

            });
        })
        .catch((error) =>{
            console.error(error);
        });
}

componentWillUnmount() {
    cancelablePromise.cancel();
}

- - 编辑 - -

通过关注 GitHub 上的问题,我发现给出的答案可能不太正确。这是我使用的一个版本,适用于我的目的:

export const makeCancelableFunction = (fn) => {
    let hasCanceled = false;

    return {
        promise: (val) => new Promise((resolve, reject) => {
            if (hasCanceled) {
                fn = null;
            } else {
                fn(val);
                resolve(val);
            }
        }),
        cancel() {
            hasCanceled = true;
        }
    };
};

这个想法是通过使函数或任何您使用的内容为 null 来帮助垃圾收集器释放内存。

你在github上有这个问题的链接吗
2021-05-23 00:23:55
2021-05-26 00:23:55
@Ren,有一个用于编辑页面和讨论问题的 GitHub站点
2021-05-29 00:23:55
我不再确定那个 GitHub 项目的确切问题在哪里。
2021-06-05 00:23:55

您可以使用AbortController取消获取请求。

另见:https : //www.npmjs.com/package/abortcontroller-polyfill

class FetchComponent extends React.Component{
  state = { todos: [] };
  
  controller = new AbortController();
  
  componentDidMount(){
    fetch('https://jsonplaceholder.typicode.com/todos',{
      signal: this.controller.signal
    })
    .then(res => res.json())
    .then(todos => this.setState({ todos }))
    .catch(e => alert(e.message));
  }
  
  componentWillUnmount(){
    this.controller.abort();
  }
  
  render(){
    return null;
  }
}

class App extends React.Component{
  state = { fetch: true };
  
  componentDidMount(){
    this.setState({ fetch: false });
  }
  
  render(){
    return this.state.fetch && <FetchComponent/>
  }
}

ReactDOM.render(<App/>, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>

所以如果你有多个fetches,你能把那个AbortController传给所有的人吗?
2021-05-31 00:23:55
我希望我知道有一个 Web API 可以取消像 AbortController 这样的请求。不过还好,现在知道还不算晚。谢谢你。
2021-06-05 00:23:55

由于该帖子已被打开,因此添加了“abortable-fetch”。 https://developers.google.com/web/updates/2017/09/abortable-fetch

(来自文档:)

控制器+信号机动 满足AbortController和AbortSignal:

const controller = new AbortController();
const signal = controller.signal;

控制器只有一种方法:

控制器中止();当你这样做时,它会通知信号:

signal.addEventListener('abort', () => {
  // Logs true:
  console.log(signal.aborted);
});

这个 API 是由 DOM 标准提供的,这就是整个 API。它是故意通用的,因此可以被其他 Web 标准和 JavaScript 库使用。

例如,这里是如何在 5 秒后进行获取超时:

const controller = new AbortController();
const signal = controller.signal;

setTimeout(() => controller.abort(), 5000);

fetch(url, { signal }).then(response => {
  return response.text();
}).then(text => {
  console.log(text);
});
@LexSoft 您找到问题的答案了吗?
2021-06-01 00:23:55
有趣,我会尝试这种方式。但在此之前,我会先阅读 AbortController API。
2021-06-08 00:23:55
我们是否可以只使用一个 AbortController 实例进行多次提取,这样当我们在 componentWillUnmount 中调用这个单个 AbortController 的 abort 方法时,它将取消我们组件中的所有现有提取?如果没有,这意味着我们必须为每个获取提供不同的 AbortController 实例,对吗?
2021-06-13 00:23:55
@Superdude 答案是肯定的
2021-06-18 00:23:55

这个警告的关键是你的组件有一个对它的引用,它被一些未完成的回调/Promise所持有。

为了避免在第二种模式中保持你的 isMounted 状态(让你的组件保持活动状态)的反模式,react 网站建议使用可选的 promise但是,该代码似乎也使您的对象保持活动状态。

相反,我通过使用带有嵌套绑定函数的闭包来完成它。

这是我的构造函数(typescript)...

constructor(props: any, context?: any) {
    super(props, context);

    let cancellable = {
        // it's important that this is one level down, so we can drop the
        // reference to the entire object by setting it to undefined.
        setState: this.setState.bind(this)
    };

    this.componentDidMount = async () => {
        let result = await fetch(…);            
        // ideally we'd like optional chaining
        // cancellable.setState?.({ url: result || '' });
        cancellable.setState && cancellable.setState({ url: result || '' });
    }

    this.componentWillUnmount = () => {
        cancellable.setState = undefined; // drop all references.
    }
}
这在概念上与保持 isMounted 标志没有什么不同,只是您将它绑定到闭包而不是将其挂在 this
2021-06-20 00:23:55