React-未安装组件上的setState()


92

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

由于某种原因,我的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>
      );
  }

问题是为什么在应该已经安装组件时会收到此错误(如从componentDidMount调用的那样),我认为一旦安装组件就可以安全地设置状态?


在我的构造函数中,我正在设置“ this.loadSearches = this.loadSearches.bind(this);” -我在问题中加了点
Marty 2015年

您是否尝试过在构造函数中将loading设置为null?那可能行得通。this.state = { loading : null };
Pramesh Bajracharya

Answers:


69

不看渲染功能有点困难。尽管已经可以发现应该执行的操作,但是每次使用间隔时,都必须在卸载时清除它。所以:

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

希望这会有所帮助,如果这样做不起作用,请提供渲染功能。

干杯


2
布鲁诺,你不能只是测试“ this”上下文的存在.. ala this && this.setState .....
james emanon

6
或者简单地:componentWillUnmount() { clearInterval(this.loadInterval); }
Greg Herbowicz

@GregHerbowicz如果使用计时器卸下和安装组件,即使进行了简单的清除操作,仍然可以将其触发。
corlaez

14

问题是为什么在应该已经安装组件时会收到此错误(如从componentDidMount调用的那样),我认为一旦安装组件就可以安全地设置状态?

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

因此,已接受的答案将保护您。如果您使用的其他异步API不允许您取消异步功能(已提交给某些处理程序),则可以执行以下操作:

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

尽管这确实消除了您在所有情况下报告的错误消息,但确实感觉像是在扫地的东西,特别是如果您的API提供了取消功能(与setInterval一样clearInterval)。


12
isMounted是一种反模式,Facebook的建议不要使用:facebook.github.io/react/blog/2015/12/16/...
马蒂

1
是的,我确实说“这确实像在地毯下扫东西”。
Marcus Junius Brutus

5

对于需要其他选择的用户,可以使用ref属性的回调方法。handleRef的参数是对div DOM元素的引用。

有关ref和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>
 )
}

5
使用ref来有效地“ isMounted”与使用isMounted完全相同,但是不清楚。isMounted不是反模式,因为它的名称是因为它是反模式,用于保存对未安装组件的引用。
Pajn

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() {
    ...
  }
}

有没有一种方法可以实现此功能组件?@john_per
Tamjid

对于功能组件,我将使用ref:const _isMounted = useRef(false); @Tamjid
john_per

1

为了后代,

在我们的案例中,此错误与Reflux,回调,重定向和setState有关。我们将setState发送到onDone回调,但也将重定向发送到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的isMounted是“已弃用/反模式”,因此我们采用了_mount变量并对其进行监视。


1

共享一个由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中的闭包

总的来说,以上想法与react doc建议的makeCancelable方法很接近,该方法明确指出

isMounted是一个反模式

信用

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.