Prevent React setState on unmounted Component

2019-06-12 admin

背景

熟悉 React 的小伙伴对这个错误信息一定不陌生:

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

之前也看到过, 但是一直没处理。 早上趁喝茶的功夫决定看一下。

复现的路径: 在某个请求结果没回来之前就切换页面, 必现。

原因

clipboard.png

这种问题出现一般是, 请求发出得到结果之后, 进行了 setState 的操作。 形如:

class Demo extends Component {
  constructor(props) {
    super(props);

    this.state = {
      news: [],
    };
  }

  componentDidMount() {
    axios
      .get('https://hn.algolia.com/api/v1/search?query=react')
      .then(result =>
        this.setState({
          news: result.data,
        }),
      );
  }

  render() {
    return (
      <ul>
        {this.state.news.map(topic => (
          <li key={topic.objectID}>{topic.title}</li>
        ))}
      </ul>
    );
  }
}

在结果回来之前, 我们切换路由, 组件销毁, 但是请求是异步的, 结果回来之后, 组件已经销毁了, 这个之后执行 setState 可能会有意想不到的后果, 正如error 信息提示的那样.

知道原因之后, 就比较好解决了:

找资料的过程中也发现了一些 脑洞大开的解决办法:

几种开脑洞的解决办法

标志位法(不推荐)

这个方法的思路也很简单, 给个标志位, 比如叫 _isMounted

class Demo extends Component {
  _isMounted = false;

  constructor(props) {
    super(props);

    this.state = {
      news: [],
    };
  }

  componentDidMount() {
    this._isMounted = true;

    axios
      .get('https://hn.algolia.com/api/v1/search?query=react')
      .then(result => {
        if (this._isMounted) {
          this.setState({
            news: result.data.hits,
          });
        }
      });
  }

  componentWillUnmount() {
    this._isMounted = false;
  }

  render() {
     // ...
  }
}

Unmount 之后, 标志位为false , 不执行setState. 自然也就不会报错。

顺着这个思路,干脆写个组件统一处理, 形如

import React from 'react';

function inject_prevent_setState_after_unount(target) {
  let componentWillUnmount = target.prototype.componentWillUnmount;
  target.prototype.componentWillUnmount = function() {
    if (componentWillUnmount) componentWillUnmount.call(this, ...arguments);
    this.unmount = true;
  };

  let setState = target.prototype.setState;
  target.prototype.setState = function() {
    if (this.unmount) return;
    setState.call(this, ...arguments);
  };
}

@inject_prevent_setState_after_unount
class BaseComponent extends React.Component {}

export default BaseComponent;

然后在业务代码里直接继承这个组件, 这个和上面的标志位法其实是一个道理,虽然也能 hack 掉错误信息, 但是请求的副作用依旧会发生,这种做法也是不推荐的。

官网上也有对这个情景的描述:

详情请戳我

The primary use case for isMounted() is to avoid calling setState() after a component has unmounted, because calling setState() after a component has unmounted will emit a warning. The “setState warning” exists to help you catch bugs, because calling setState() on an unmounted component is an indication that your app/component has somehow failed to clean up properly. Specifically, calling setState() in an unmounted component means that your app is still holding a reference to the component after the component has been unmounted - which often indicates a memory leak!

To avoid the error message, people often add lines like this:

if (this.isMounted()) { // This is bad. this.setState({...}); }

官网推荐的做法是: 在componentWillUnmount 的时候取消掉所有的请求。

形如:


class MyComponent extends React.Component {
  componentDidMount() {
    mydatastore.subscribe(this);
  }
  render() {
    ...
  }
  componentWillUnmount() {
    mydatastore.unsubscribe(this);
  }
}

实现一个可以 cancel 的 Promise

我在这也给出一个简单的实现:

// cancelablePromise.js
export const cancelablePromise = promise => {
  let hasCanceled = false;

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

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

在你的组件中:


import React, { Component } from "react";
import cancelablePromise from "./cancelable-promise";

class MyComponent extends Component {
  state = {
    data: [],
    error: null,
  };

  pendingPromises = [];

  componentWillUnmount = () =>
    this.pendingPromises.map(p => p.cancel());

  appendPendingPromise = promise =>
    this.pendingPromises = [...this.pendingPromises, promise];

  removePendingPromise = promise =>
    this.pendingPromises = this.pendingPromises.filter(p => p !== promise);

  handleOnClick = () => {
    const wrappedPromise = cancelablePromise(fetchData());
    this.appendPendingPromise(wrappedPromise);

    return wrappedPromise.promise
      .then(() => this.setState({ data }))
      .then(() => this.removePendingPromise(wrappedPromise))
      .catch(errorInfo => {
        if (!errorInfo.isCanceled) {
          this.setState({ error: errorInfo.error });
          this.removePendingPromise(wrappedPromise);
        }
      });
  }

  render() {
    const { data, error } = this.state;

    if (error) {
      return (
        <div className="error">
          There was an error fetching data: {error}
        </div>
      );
    }

    return (
      <div className="data">
        <button onClick={this.handleClick}>reload data!</button>
        <ul className="data-list">
          {data.map((item, i) => <li key={i}>{item}</li>)}
        </ul>
      </div>
    );
  }
}

这样就保证了setState 的时候, 上下文是完整的, 进而从根本上解决报错的问题。

结语

上面介绍了解决warning几种方式,最好的办法当然是用cancelablePromise来处理, 但是这种方式有一定的侵入性,带来了额外的开发成本。 如果你实在不能忍受那个报错, 可以使用这种方式, 当然也可以选择无视它

具体如何选择还需要各位看官老爷自行斟酌。

以上, 希望对大家有所启发, 谢谢。

参考资料:

https://reactjs.org/blog/2015… https://github.com/facebook/r…

[转载]原文链接:https://segmentfault.com/a/1190000019456742

本站文章除注明转载外,均为本站原创或编译。欢迎任何形式的转载,但请务必注明出处。

转载请注明:文章转载自 JavaScript中文网 [https://www.javascriptcn.com]

本文地址:https://www.javascriptcn.com/read-67142.html

文章标题:Prevent React setState on unmounted Component

相关文章
AngularJS:何时应该使用Directive、Controller、Servic
AngularJS是一款非常强大的前端MVC框架。同时,它也引入了相当多的概念,这些概念我们可能不是太熟悉。(译者注:老外真谦虚,我大天朝的码农对这些概念那是相当熟悉啊!)这些概念有: Directive(指令) Controller(控制...
2015-11-11
jquery拼接ajax 的json和字符串拼接的方法
整理文档,搜刮出一个jquery拼接ajax 的json和字符串拼接的代码,稍微整理精简一下做下分享。 jQuery拼接字符串ajax &lt;form id=&quot;myForm&quot; action=&quot;#&quot;&...
2017-04-01
VSCode配置react开发环境的步骤
vscode 默认配置对于 react 的 JSX 语法不友好,体现在使用自动格式化或者粘贴后默认缩进错误,尽管可以通过改变 language mode 缓解错误,但更改 language mode 后的格式化依然不够理想。 通过搭配使用 ...
2017-12-28
ionic2 tabs 图标自定义实例
一、准备资源 tabs icon 的svg格式的矢量图片 二、生成字体样式文件 打开icoMoon网站去制作字体文件。 三、使用字体文件 解压下载的文件,将其中的fonts文件夹拷贝到ionic2项目的src/assest目录下。并...
2017-03-13
使用 Protocol Buffers 代替 JSON 的五个原因
在 Ruby 和 Rails 开发者中,面向服务 (Service-Oriented) 架构有一个当之无愧的名声,它是一个缓解程序规模恶性增长的一个强有力的途径,可在大量应用程序中提取关注点。这些新生小巧的服务通常继续使用 Rails 或 ...
2016-01-13
Node.js实现Excel转JSON
一直在做一个关于网上选课的系统,选用了时下比较流行的node.js。今天在想怎么把学生或者老师的信息导入进去,涉及数量比较多一点,我手边又正好有一部分excel的表格。就想把excel转成json然后倒入到mongodb中去。 搜了下网上的...
2017-03-23
HTML 5 <em> <strong> <dfn> <code>
&lt;em&gt; 把文本定义为强调的内容。 &lt;strong&gt; 把文本定义为语气更强的强调的内容。 &lt;dfn&gt; 定义一个定义项目。 &lt;code&gt; 定义计算机代码文本。 &lt;samp&gt; 定义样本...
2015-11-12
数据格式之战:JSON vs XML
在比较JSON和XML之前,我们先来上一堂关于数据格式的简要历史(更准确的说,是关于XML的始祖): 早在1970年,IBM开发了一种叫Generalized Markup Language的标记语言,简称GML,它主要是为脚本语言定义的一...
2016-01-13
vue2 如何实现div contenteditable=“true”(类似于v-model)的效果
发现问题 在 vue2 中对表单控件有着良好的双向数据绑定机制,但是对于要特定实现某些功能的输入时,我们就不得不使用到 contenteditable=“true” 的 div ,而在这个 div 上是使用 v-model 是没有效果的。那...
2017-03-20
angular+ionic 的app上拉加载更新数据实现方法
第一步,首先需要在&lt;ion-content&gt;标签里面加入标签&lt;ion-infinite-scroll ng-if=&quot;hasmore&quot; on-infinite=&quot;loadMore()&quot;...
2017-03-07
回到顶部