How to cancel a fetch on componentWillUnmount

时间:2018-04-18 18:15:58

标签: reactjs react-native

I think the title says it all. The yellow warning is displayed every time I unmount a component that is still fetching.

Warning: Can't call setState (or forceUpdate) on an unmounted component. This is a no-op, but ... To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.

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

12 个答案:

答案 0 :(得分:40)

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

这就是为什么最好将部分异步逻辑移出组件。

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

componentDidMount(){
  this.mounted = true;

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

componentWillUnmount(){
  this.mounted = false;
}

我会再强调一下 - 这个is an antipattern但在你的情况下可能已经足够了(就像他们对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(() => {
    isMounted.current = true;
    return () => {
      isMounted.current = false;
    };
  }, []);

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

  return value;
}

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

答案 1 :(得分:14)

React recommend的友好人员将您的获取呼叫/承诺包装在可取消的承诺中。尽管在该文档中没有建议通过获取将代码与类或函数分开,但这似乎是可取的,因为其他类和函数可能需要此功能,但是代码重复是一种反模式,无论代码如何缠结应该在componentWillUnmount()中处置或取消。按照React的要求,您可以在cancel()中的包装承诺中调用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来帮助垃圾收集器释放内存。

答案 2 :(得分:10)

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

答案 3 :(得分:7)

由于该职位已被打开,所以添加了“ 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);
});

答案 4 :(得分:3)

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

答案 5 :(得分:3)

这个警告的关键在于你的组件有一个引用它的一些未完成的回调/承诺。

为避免反模式保持你的isMounted状态(使你的组件保持活着),就像在第二种模式中所做的那样,反应网站建议using an optional promise;但是,该代码似乎也可以让您的对象保持活力。

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

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

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

答案 6 :(得分:1)

我认为如果没有必要通知服务器取消-最好的方法是使用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);
  }
}

答案 7 :(得分:0)

除了已接受的解决方案中的可取消的Promise挂钩示例外,还可以使用一个useAsyncCallback挂钩来包装请求回调并返回可取消的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
}

答案 8 :(得分:0)

使用CPromise软件包,您可以取消承诺链,包括嵌套的。它支持AbortController和生成器,以替代ECMA异步功能。使用CPromise装饰器,您可以轻松管理异步任务,使其可以取消。

装饰器用法Live Demo

import { async, listen, cancel, timeout } from "c-promise2";
import cpFetch from "cp-fetch";

export class TestComponent extends React.Component {
  state = {
    text: ""
  };

  @timeout(5000)
  @listen
  @async
  *componentDidMount() {
    console.log("mounted");
    const response = yield cpFetch(this.props.url);
    this.setState({ text: `json: ${yield response.text()}` });
  }

  render() {
    return <div>{this.state.text}</div>;
  }

  @cancel()
  componentWillUnmount() {
    console.log("unmounted");
  }
}

所有阶段都完全可以取消/中止。 这是与React Live Demo

一起使用的示例
export class TestComponent extends React.Component {
  state = {};

  async componentDidMount() {
    console.log("mounted");
    this.controller = new CPromise.AbortController();
    try {
      const json = await this.myAsyncTask(
        "https://run.mocky.io/v3/7b038025-fc5f-4564-90eb-4373f0721822?mocky-delay=2s"
      );
      console.log("json:", json);
      await this.myAsyncTaskWithDelay(1000, 123); // just another async task
      this.setState({ text: JSON.stringify(json) });
    } catch (err) {
      if (CPromise.isCanceledError(err)) {
        console.log("tasks terminated");
      }
    }
  }

  myAsyncTask(url) {
    return CPromise.from(function* () {
      const response = yield cpFetch(url); // cancellable request
      return yield response.json();
    }).listen(this.controller.signal);
  }

  myAsyncTaskWithDelay(ms, value) {
    return new CPromise((resolve, reject, { onCancel }) => {
      const timer = setTimeout(resolve, ms, value);
      onCancel(() => {
        console.log("timeout cleared");
        clearTimeout(timer);
      });
    }).listen(this.controller.signal);
  }

  render() {
    return (
      <div>
        AsyncComponent: <span>{this.state.text || "fetching..."}</span>
      </div>
    );
  }
  componentWillUnmount() {
    console.log("unmounted");
    this.controller.abort(); // kill all pending tasks
  }
}

Using Hooks and cancel method

import React, { useEffect, useState } from "react";
import CPromise from "c-promise2";
import cpFetch from "cp-fetch";

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

  useEffect(() => {
    console.log("mount");
    // all stages here are completely cancellable
    const promise = cpFetch(props.url)
      .then(function* (response) {
        const json = yield response.json();
        setText(`Delay for 2000ms...`);
        yield CPromise.delay(2000);
        setText(`Success: ${JSON.stringify(json)}`);
      })
      .canceled()
      .catch((err) => {
        setText(`Failed: ${err}`);
      });

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

  return <p>{text}</p>;
}

答案 9 :(得分:0)

另一种替代方法是将异步函数包装在一个包装器中,该包装器将在组件卸载时处理用例

正如我们所知,函数也是 js 中的对象,因此我们可以使用它们来更新闭包值

const promesifiedFunction1 = (func) => {
  return function promesify(...agrs){
    let cancel = false;
    promesify.abort = ()=>{
      cancel = true;
    }
    return new Promise((resolve, reject)=>{
       function callback(error, value){
          if(cancel){
              reject({cancel:true})
          }
          error ? reject(error) : resolve(value);
       }
       agrs.push(callback);
       func.apply(this,agrs)
    })
  }
}

//here param func pass as callback should return a promise object
//example fetch browser API
//const fetchWithAbort = promesifiedFunction2(fetch)
//use it as fetchWithAbort('http://example.com/movies.json',{...options})
//later in componentWillUnmount fetchWithAbort.abort()
const promesifiedFunction2 = (func)=>{
  return async function promesify(...agrs){
    let cancel = false;
    promesify.abort = ()=>{
      cancel = true;
    }

    try {
      const fulfilledValue = await func.apply(this,agrs);
      if(cancel){
        throw 'component un mounted'
      }else{
        return fulfilledValue;
      }
    }
    catch (rejectedValue) {
      return rejectedValue
    }
  }
}

然后在 componentWillUnmount() 内简单地调用 promesifiedFunction.abort() 这将更新取消标志并运行拒绝功能

答案 10 :(得分:0)

只需四步:

1.创建 AbortController::const controller = new AbortController() 实例

2.get signal::const signal = controller.signal

3.通过信号获取参数

4.controller 随时中止:: controller.abort();

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

function beginFetching() {
    var urlToFetch = "https://xyxabc.com/api/tt";

    fetch(urlToFetch, {
            method: 'get',
            signal: signal,
        })
        .then(function(response) {
            console.log('Fetch complete');
        }).catch(function(err) {
            console.error(` Err: ${err}`);
        });
}


function abortFetching() {
    controller.abort()
}

答案 11 :(得分:-1)

我想我找到了解决方法。问题不在于获取本身,而是在组件被解除后的setState。因此,解决方案是将this.state.isMounted设置为false,然后将componentWillMount设置为true,并将componentWillUnmount设置为false。然后只需if(this.state.isMounted) fetch中的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,
    })
  }