React - 在已卸载的组件

时间:2015-10-02 08:09:14

标签: javascript ajax reactjs state

在我的react组件中,我试图在ajax请求正在进行时实现一个简单的微调器 - 我使用state来存储加载状态。

出于某种原因,我的React组件中的这段代码会抛出此错误

  

只能更新已安装或安装的组件。这通常意味着   你在一个卸载的组件上调用了setState()。这是一个无操作。   请检查未定义组件的代码。

如果我摆脱了第一个setState调用,那么错误就会消失。



constructor(props) {
  super(props);
  this.loadSearches = this.loadSearches.bind(this);

  this.state = {
    loading: false
  }
}

loadSearches() {

  this.setState({
    loading: true,
    searches: []
  });

  console.log('Loading Searches..');

  $.ajax({
    url: this.props.source + '?projectId=' + this.props.projectId,
    dataType: 'json',
    crossDomain: true,
    success: function(data) {
      this.setState({
        loading: false
      });
    }.bind(this),
    error: function(xhr, status, err) {
      console.error(this.props.url, status, err.toString());
      this.setState({
        loading: false
      });
    }.bind(this)
  });
}

componentDidMount() {
  setInterval(this.loadSearches, this.props.pollInterval);
}

render() {

    let searches = this.state.searches || [];


    return (<div>
          <Table striped bordered condensed hover>
          <thead>
            <tr>
              <th>Name</th>
              <th>Submit Date</th>
              <th>Dataset &amp; Datatype</th>
              <th>Results</th>
              <th>Last Downloaded</th>
            </tr>
          </thead>
          {
          searches.map(function(search) {

                let createdDate = moment(search.createdDate, 'X').format("YYYY-MM-DD");
                let downloadedDate = moment(search.downloadedDate, 'X').format("YYYY-MM-DD");
                let records = 0;
                let status = search.status ? search.status.toLowerCase() : ''

                return (
                <tbody key={search.id}>
                  <tr>
                    <td>{search.name}</td>
                    <td>{createdDate}</td>
                    <td>{search.dataset}</td>
                    <td>{records}</td>
                    <td>{downloadedDate}</td>
                  </tr>
                </tbody>
              );
          }
          </Table >
          </div>
      );
  }
&#13;
&#13;
&#13;

问题是为什么我应该已经安装组件时出现此错误(因为它是从componentDidMount调用的)我认为安装组件后设置状态是安全的吗?

7 个答案:

答案 0 :(得分:61)

没有看到渲染功能有点困难。虽然已经可以发现你应该做的事情,但每次你使用一个间隔时你必须在卸载时清除它。所以:

componentDidMount() {
    this.loadInterval = setInterval(this.loadSearches, this.props.pollInterval);
}

componentWillUnmount () {
    this.loadInterval && clearInterval(this.loadInterval);
    this.loadInterval = false;
}

由于在卸载后仍可能会调用这些成功和错误回调,因此可以使用interval变量来检查它是否已挂载。

this.loadInterval && this.setState({
    loading: false
});

希望这会有所帮助,如果没有这项工作,请提供渲染功能。

干杯

答案 1 :(得分:12)

  

问题是为什么我应该已经安装组件时出现此错误(因为它是从componentDidMount调用的)我认为安装组件后设置状态是安全的吗?

componentDidMount调用 。您的componentDidMount会生成一个回调函数,该函数将在计时器处理程序的堆栈中执行,而不是在componentDidMount的堆栈中执行。显然,当你的回调(this.loadSearches)被执行时,组件已卸载。

所以接受的答案会保护你。如果您使用的某些其他异步API不允许您取消异步函数(已经提交给某个处理程序),您可以执行以下操作:

if (this.isMounted())
     this.setState(...

这将消除您在所有情况下报告的错误消息,尽管它确实感觉像是在地毯下扫地,特别是如果您的API提供取消功能(setIntervalclearInterval一样)

答案 2 :(得分:5)

对于谁需要另一个选项,ref属性的回调方法可以是一种解决方法。 handleRef的参数是对div DOM元素的引用。

有关refs和DOM的详细信息:https://facebook.github.io/react/docs/refs-and-the-dom.html

handleRef = (divElement) => {
 if(divElement){
  //set state here
 }
}

render(){
 return (
  <div ref={this.handleRef}>
  </div>
 )
}

答案 3 :(得分:3)

class myClass extends Component {
  _isMounted = false;

  constructor(props) {
    super(props);

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

  componentDidMount() {
    this._isMounted = true;
    this._getData();
  }

  componentWillUnmount() {
    this._isMounted = false;
  }

  _getData() {
    axios.get('https://example.com')
      .then(data => {
        if (this._isMounted) {
          this.setState({ data })
        }
      });
  }


  render() {
    ...
  }
}

答案 4 :(得分:1)

对后代来说,

在我们的例子中,这个错误与Reflux,回调,重定向和setState有关。我们向onDone回调发送了一个setState,但我们还向onSuccess回调发送了一个重定向。在成功的情况下,我们的onSuccess回调在onDone 之前执行。此会在尝试的setState 之前导致重定向。因此错误,setState在未安装的组件上。

回流商店行动:

generateWorkflow: function(
    workflowTemplate,
    trackingNumber,
    done,
    onSuccess,
    onFail)
{...

修复前调用:

Actions.generateWorkflow(
    values.workflowTemplate,
    values.number,
    this.setLoading.bind(this, false),
    this.successRedirect
);

修复后调用:

Actions.generateWorkflow(
    values.workflowTemplate,
    values.number,
    null,
    this.successRedirect,
    this.setLoading.bind(this, false)
);

更多

在某些情况下,由于React的已安装是&#34;已弃用/反模式&#34;,我们已采用_mounted变量并自行监控。

答案 5 :(得分:0)

共享由react hooks启用的解决方案。

React.useEffect(() => {
  let isSubscribed = true

  callApi(...)
    .catch(err => isSubscribed ? this.setState(...) : Promise.reject({ isSubscribed, ...err }))
    .then(res => isSubscribed ? this.setState(...) : Promise.reject({ isSubscribed }))
    .catch(({ isSubscribed, ...err }) => console.error('request cancelled:', !isSubscribed))

  return () => (isSubscribed = false)
}, [])

相同的解决方案可以扩展到您希望在获取id更改时取消的先前请求时,否则,多个进行中的请求(this.setState订单)。

React.useEffect(() => {
  let isCancelled = false

  callApi(id).then(...).catch(...) // similar to above

  return () => (isCancelled = true)
}, [id])

这要感谢javascript中的closures

通常,以上想法与react doc建议的makeCancelable approach很接近,

  

isMounted是反模式

信用

https://juliangaramendy.dev/use-promise-subscription/

答案 6 :(得分:0)

仅供参考。将CPromise与修饰符结合使用,可以完成以下技巧: (Live demo here

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

  @canceled(function (err) {
    console.warn(`Canceled: ${err}`);
    if (err.code !== E_REASON_DISPOSED) {
      this.setState({ text: err + "" });
    }
  })
  @listen
  @async
  *componentDidMount() {
    console.log("mounted");
    const json = yield this.fetchJSON(
      "https://run.mocky.io/v3/7b038025-fc5f-4564-90eb-4373f0721822?mocky-delay=2s"
    );
    this.setState({ text: JSON.stringify(json) });
  }

  @timeout(5000)
  @async
  *fetchJSON(url) {
    const response = yield cpFetch(url); // cancellable request
    return yield response.json();
  }

  render() {
    return (
      <div>
        AsyncComponent: <span>{this.state.text || "fetching..."}</span>
      </div>
    );
  }

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