如何取消componentWillUnmount上的提取

Joã*_*elo 73 reactjs

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

警告:无法在卸载的组件上调用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);
      });
  }
Run Code Online (Sandbox Code Playgroud)

Tom*_*zyk 56

当您触发Promise时,它可能需要几秒钟才能结算,到那时用户可能已导航到您应用中的其他位置.因此,当Promise解析setState在未安装的组件上执行时,您会收到错误 - 就像您的情况一样.这也可能导致内存泄漏.

这就是为什么最好将一些异步逻辑从组件中移出.

否则,你需要以某种方式取消你的承诺.或者 - 作为最后的手段技术(它是反模式) - 你可以保留一个变量来检查组件是否仍然被挂载:

componentDidMount(){
  this.mounted = true;

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

componentWillUnmount(){
  this.mounted = false;
}
Run Code Online (Sandbox Code Playgroud)

我将再次强调 - 这是一个反模式,但在你的情况下可能就足够了(就像他们对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;
}
Run Code Online (Sandbox Code Playgroud)

选项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
}
Run Code Online (Sandbox Code Playgroud)

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

  • 请参阅[isMounted是一个反模式](https://reactjs.org/blog/2015/12/16/ismounted-antipattern.html)和[中止获取](https://github.com/whatwg/fetch/issues/447). (8认同)
  • “这就是为什么最好将异步逻辑移出组件的原因。”是什么意思?反应中的一切不是一个组件吗? (5认同)
  • 所以没有真正的方法来取消对componentWillUnmount的提取? (2认同)
  • 更仔细地阅读反模式链接。使用“isMounted()”方法是一种反模式,但建议使用“_isMounted”等属性。 (2认同)

hal*_*onj 17

React的友好人员建议将您的提取电话/承诺包装在可取消的承诺中.虽然该文档中没有建议使用fetch将代码与类或函数分开,但这似乎是可取的,因为其他类和函数可能需要此功能,代码重复是反模式,并且无论延迟代码如何应该被处置或取消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();
}
Run Code Online (Sandbox Code Playgroud)

----编辑----

通过在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;
        }
    };
};
Run Code Online (Sandbox Code Playgroud)

这个想法是通过使函数或任何你使用null的东西来帮助垃圾收集器释放内存.

  • GitHub 问题链接:https://github.com/facebook/react/issues/5465 (2认同)

小智 15

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

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'))
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)

  • 我希望我知道有一个 Web API 可以像 AbortController 这样取消请求。但好吧,现在知道还不算太晚。谢谢。 (2认同)

Ben*_*aki 9

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

(来自文档:)

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

const controller = new AbortController();
const signal = controller.signal;
Run Code Online (Sandbox Code Playgroud)

控制器只有一种方法:

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

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

该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);
});
Run Code Online (Sandbox Code Playgroud)

  • @Superdude 答案是肯定的 (2认同)