如何取消对componentWillUnmount的提取


90

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

安慰

警告:无法在未安装的组件上调用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);
      });
  }

这是什么警告,我没有这个问题
nima moradi

问题更新
若昂·贝洛

您是否答应过获取或异步获取代码
nima moradi

将您获取的代码添加到问题中
nima moradi18 '18 -4-18

Answers:


80

当您触发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


4
所以没有真正的方法来取消对componentWillUnmount的提取?
若昂·贝洛

1
哦,我之前没有注意到您答案的代码,它确实有效。感谢
若昂·贝洛


2
您的意思是“这就是为什么最好将异步逻辑移出组件”。难道不是所有的成分都在起作用吗?
卡皮克18/09/14

1
@Tomasz Mularczyk非常感谢,您做了值得的工作。
KARTHIKEYAN.18年

25

React的友好人员建议将您的提取调用/承诺包装在可取消的承诺中。尽管在该文档中没有建议使用提取将代码与类或函数分开,但这似乎是可取的,因为其他类和函数可能需要此功能,但是代码重复是一种反模式,无论代码如何缠结应该在中处置或取消componentWillUnmount()。根据React,您可以调用cancel()已包装的Promise,componentWillUnmount以避免在未安装的组件上设置状态。

如果我们使用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上有此问题的链接
Ren

@Ren,有一个GitHub网站可编辑页面并讨论问题。
haleonj

我不再确定该GitHub项目的确切问题在哪里。
haleonj


22

您可以使用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>


2
我希望我知道有一个Web API可以取消AbortController之类的请求。但是,知道它还为时不晚。谢谢。
Lex Soft

11

由于该职位已被打开,所以添加了“ abortable-fetch”。 https://developers.google.com/web/updates/2017/09/abortable-fetch

(来自文档:)

控制器+信号操纵满足AbortController和AbortSignal:

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

控制器只有一种方法:

controller.abort(); 当您这样做时,它会通知信号:

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);
});

有趣的是,我将尝试这种方式。但在此之前,我将首先阅读AbortController API。
Lex Soft

我们是否可以仅使用一个AbortController实例进行多次提取,以便当我们在componentWillUnmount中调用单个AbortController的abort方法时,它将取消组件中所有现有的提取?如果不是,则意味着我们必须为每个提取提供不同的AbortController实例,对吗?
Lex Soft

3

该警告的症结在于,您的组件对它的引用由一些未完成的回调/承诺持有。

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

相反,我通过使用闭包(对setState嵌套嵌套的绑定函数)来完成此操作。

这是我的构造函数(打字稿)…

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.
    }
}

3
从概念上讲,这与保留isMounted标志没有什么不同,只是将其绑定到闭包而不是挂起它this
AnilRedshift

2

当我需要“取消所有订阅并取消同步”时,我通常向componentWillUnmount中的redux分发一些内容,以通知所有其他订阅者,并在必要时向服务器发送一个有关取消的其他请求


2

我认为如果没有必要通知服务器取消-最好的方法是使用async / await语法(如果可用)。

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

async componentDidMount() {
  try {
    const responseJson = await fetch('LINK HERE')
      .then((response) => response.json());

    this.setState({
      isLoading: false,
      dataSource: responseJson,
    }
  } catch {
    console.error(error);
  }
}

0

除了接受的解决方案中的cancellable promise hooks示例之外,拥有一个useAsyncCallback包装请求回调并返回cancellable promise的钩子也很方便。想法是一样的,但是钩子就像常规的钩子一样工作useCallback。这是一个实现示例:

function useAsyncCallback<T, U extends (...args: any[]) => Promise<T>>(callback: U, dependencies: any[]) {
  const isMounted = useRef(true)

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

  const cb = useCallback(callback, dependencies)

  const cancellableCallback = useCallback(
    (...args: any[]) =>
      new Promise<T>((resolve, reject) => {
        cb(...args).then(
          value => (isMounted.current ? resolve(value) : reject({ isCanceled: true })),
          error => (isMounted.current ? reject(error) : reject({ isCanceled: true }))
        )
      }),
    [cb]
  )

  return cancellableCallback
}

-2

我想我想出了一种解决方法。问题不在于提取本身,而在于组件被关闭后的setState。因此,解决办法是设置this.state.isMountedfalse,然后componentWillMount将其更改为真,并在componentWillUnmount重新设置为FALSE。然后只if(this.state.isMounted)在内部获取setState。像这样:

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

  componentDidMount(){
    this.setState({
      isMounted: true,
    })

    return fetch('LINK HERE')
      .then((response) => response.json())
      .then((responseJson) => {
        if(this.state.isMounted){
          this.setState({
            isLoading: false,
            dataSource: responseJson,
          }, function(){
          });
        }
      })
      .catch((error) =>{
        console.error(error);
      });
  }

  componentWillUnmount() {
    this.setState({
      isMounted: false,
    })
  }

3
setState可能不是理想的,因为它不会立即更新state的值。
LeonF '18
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.