如何取消对 componentWillUnmount 的提取

Posted

技术标签:

【中文标题】如何取消对 componentWillUnmount 的提取【英文标题】:How to cancel a fetch on componentWillUnmount 【发布时间】:2018-09-29 02:13:57 【问题描述】:

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

安慰

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

【问题讨论】:

警告我没有这个问题 问题已更新 你是否承诺或异步获取代码 加你获取代码到qustion 见isMounted is an Antipattern和aborting a fetch。 【参考方案1】:

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

【讨论】:

所以没有真正的方法可以取消对 componentWillUnmount 的提取? 哦,我之前没有注意到你的答案的代码,它确实有效。谢谢 见isMounted is an Antipattern和aborting a fetch。 “这就是为什么最好将异步逻辑移出组件。”是什么意思?反应中的一切不都是一个组件吗? @Karpik 我的意思是使用 redux 或 mobx 或其他状态管理库。然而,像 react-suspense 这样的新功能可能会解决这个问题。【参考方案2】:

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

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

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,有一个 GitHub site 用于编辑页面和讨论问题。 我不再确定 GitHub 项目的确切问题出在哪里。 GitHub 问题链接:github.com/facebook/react/issues/5465【参考方案3】:

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

【讨论】:

我希望我知道有一个用于取消请求的 Web API,例如 AbortController。不过好吧,现在知道还为时不晚。谢谢。 那么如果你有多个fetches,你能把那个AbortController传给他们所有人吗?【参考方案4】:

自从帖子被打开后,添加了一个“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。它是特意通用的,因此可以被其他网络标准和 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。 我们是否可以只使用一个 AbortController 实例进行多次提取,这样当我们在 componentWillUnmount 中调用单个 AbortController 的 abort 方法时,它会取消我们组件中所有现有的提取?如果没有,这意味着我们必须为每个提取提供不同的 AbortController 实例,对吧? @LexSoft 您找到问题的答案了吗? @Superdude 答案是肯定的【参考方案5】:

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

为了避免像第二种模式那样保持 isMounted 状态的反模式(使组件保持活动状态),react 网站建议using an optional 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.
    

【讨论】:

这在概念上与保留一个 isMounted 标志没有什么不同,只是你将它绑定到闭包而不是挂在this【参考方案6】:

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

【讨论】:

【参考方案7】:

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

【讨论】:

【参考方案8】:

除了可接受的解决方案中的可取消承诺挂钩示例之外,使用 useAsyncCallback 挂钩包装请求回调并返回可取消承诺会很方便。想法是一样的,但钩子的工作方式与普通的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

【讨论】:

【参考方案9】:

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

我们知道函数也是 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】:

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

装饰器使用Live Demo:

import React from "react";
import  ReactComponent, timeout  from "c-promise2";
import cpFetch from "cp-fetch";

@ReactComponent
class TestComponent extends React.Component 
  state = 
    text: "fetching..."
  ;

  @timeout(5000)
  *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>;
  

  componentWillUnmount() 
    console.log("unmounted");
  

那里的所有阶段都是完全可取消/可中止的。 这是一个使用 React 的例子Live Demo

import React,  Component  from "react";
import 
  CPromise,
  CanceledError,
  ReactComponent,
  E_REASON_UNMOUNTED,
  listen,
  cancel
 from "c-promise2";
import cpAxios from "cp-axios";

@ReactComponent
class TestComponent extends Component 
  state = 
    text: ""
  ;

  *componentDidMount(scope) 
    console.log("mount");
    scope.onCancel((err) => console.log(`Cancel: $err`));
    yield CPromise.delay(3000);
  

  @listen
  *fetch() 
    this.setState( text: "fetching..." );
    try 
      const response = yield cpAxios(this.props.url).timeout(
        this.props.timeout
      );
      this.setState( text: JSON.stringify(response.data, null, 2) );
     catch (err) 
      CanceledError.rethrow(err, E_REASON_UNMOUNTED);
      this.setState( text: err.toString() );
    
  

  *componentWillUnmount() 
    console.log("unmount");
  

  render() 
    return (
      <div className="component">
        <div className="caption">useAsyncEffect demo:</div>
        <div>this.state.text</div>
        <button
          className="btn btn-success"
          type="submit"
          onClick=() => this.fetch(Math.round(Math.random() * 200))
        >
          Fetch random character info
        </button>
        <button
          className="btn btn-warning"
          onClick=() => cancel.call(this, "oops!")
        >
          Cancel request
        </button>
      </div>
    );
  

Using Hooks and cancel method

import React,  useState  from "react";
import 
  useAsyncEffect,
  E_REASON_UNMOUNTED,
  CanceledError
 from "use-async-effect2";
import cpAxios from "cp-axios";

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

  const cancel = useAsyncEffect(
    function* () 
      setText("fetching...");
      try 
        const response = yield cpAxios(
          `https://rickandmortyapi.com/api/character/$id`
        ).timeout(props.timeout);
        setText(JSON.stringify(response.data, null, 2));
       catch (err) 
        CanceledError.rethrow(err, E_REASON_UNMOUNTED);
        setText(err.toString());
      
    ,
    [id]
  );

  return (
    <div className="component">
      <div className="caption">useAsyncEffect demo:</div>
      <div>text</div>
      <button
        className="btn btn-success"
        type="submit"
        onClick=() => setId(Math.round(Math.random() * 200))
      >
        Fetch random character info
      </button>
      <button className="btn btn-warning" onClick=cancel>
        Cancel request
      </button>
    </div>
  );

【讨论】:

【参考方案11】:

只需四个步骤:

1.create instance of 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()

【讨论】:

【参考方案12】:

如果组件卸载时超时清除它们。

useEffect(() => 
    getReusableFlows(dispatch, selectedProject);
    dispatch(fetchActionEvents());

    const timer = setInterval(() => 
      setRemaining(getRemainingTime());
    , 1000);

    return () => 
      clearInterval(timer);
    ;
  , []);

【讨论】:

【参考方案13】:

这里有很多很棒的答案,我决定也加入一些。创建自己的 useEffect 版本来消除重复非常简单:

import  useEffect  from 'react';

function useSafeEffect(fn, deps = null) 
  useEffect(() => 
    const state =  safe: true ;
    const cleanup = fn(state);
    return () => 
      state.safe = false;
      cleanup?.();
    ;
  , deps);

将其用作正常的 useEffect 并在您传递的回调中为您提供state.safe

useSafeEffect(( safe ) => 
  // some code
  apiCall(args).then(result => 
    if (!safe) return;
    // updating the state
  )
, [dep1, dep2]);

【讨论】:

【参考方案14】:

我想我想出了一个办法。问题不在于获取本身,而在于组件关闭后的 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,
    )
  

【讨论】:

setState 可能并不理想,因为它不会立即更新 state 中的值。

以上是关于如何取消对 componentWillUnmount 的提取的主要内容,如果未能解决你的问题,请参考以下文章

为啥不会/如何对 PHP 会话变量进行排序而不是取消设置

如何取消超类对默认 Matlab 函数的覆盖

我如何对 youtube 嵌入播放器进行编程以在单击时取消静音

如何对正在进行的 RXJS 请求的取消进行单元测试

如何取消word对文档的分析,只要两个文件切换也显示分析,每次都要按esc好麻烦的,有高手能解决吗?

myeclipse如何取消对某个文件的验证